forked from getlantern/lantern
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgenerate_appcast.py
More file actions
153 lines (127 loc) · 5.08 KB
/
generate_appcast.py
File metadata and controls
153 lines (127 loc) · 5.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
#!/usr/bin/env python3
"""
generate_appcast.py
This script is used to fetch GitHub releases and emits a Sparkle-compatible appcast.xml.
It downloads each platform asset via the asset ID endpoint, parses the associated signatur
Usage:
export GITHUB_TOKEN=<token>
export BUILD_TYPE=<production|beta> (optional, defaults to production)
python3 scripts/generate_appcast.py
"""
import os
import sys
import subprocess
import tempfile
import re
import requests
import xml.etree.ElementTree as ET
# -------- Configuration --------
REPO = "getlantern/lantern"
OUT_PATH = "appcast.xml"
PLATFORMS = {
"macos": ".dmg",
"windows": ".exe",
"linux": ".AppImage",
}
# --------------------------------
def indent(elem, level=0):
"""Recursively add indentation to XML for pretty-printing."""
i = "\n" + level * " "
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = i + " "
for child in elem:
indent(child, level + 1)
if not child.tail or not child.tail.strip():
child.tail = i
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i
def main():
token = os.environ.get('GITHUB_TOKEN')
if not token:
sys.exit("Error: GITHUB_TOKEN variable is not set.")
build_type = os.environ.get('BUILD_TYPE', 'production')
api_url = f"https://api.github.com/repos/{REPO}/releases"
api_headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github.v3+json"
}
resp = requests.get(api_url, headers=api_headers)
resp.raise_for_status()
releases = resp.json()
ET.register_namespace('sparkle', 'http://www.andymatuschak.org/xml-namespaces/sparkle')
rss = ET.Element('rss', {
'version': '2.0',
'xmlns:sparkle': 'http://www.andymatuschak.org/xml-namespaces/sparkle'
})
channel = ET.SubElement(rss, 'channel')
ET.SubElement(channel, 'title').text = 'Lantern'
ET.SubElement(channel, 'description').text = 'Latest updates for Lantern'
ET.SubElement(channel, 'language').text = 'en'
signature_re = re.compile(r'sparkle:edSignature="([^"]+)"')
for release in releases:
# Always skip drafts
if release.get('draft'):
continue
# For production appcast, skip prereleases
# For beta appcast, include prereleases (beta releases are marked as prerelease)
if release.get('prerelease') and build_type != 'beta':
continue
name = release.get('name') or release.get('tag_name')
tag = release.get('tag_name', '').lstrip('v')
short_version = tag.split('-')[0]
pub_date = release.get('published_at')
assets = release.get('assets', []) or []
item = ET.SubElement(channel, 'item')
ET.SubElement(item, 'title').text = name
ET.SubElement(item, 'sparkle:version').text = tag
ET.SubElement(item, 'sparkle:shortVersionString').text = short_version
ET.SubElement(item, 'pubDate').text = pub_date
# one <enclosure> per platform asset
for os_name, ext in PLATFORMS.items():
asset = next((a for a in assets if a.get('name', '').endswith(ext)), None)
if not asset:
continue
asset_id = asset['id']
download_url = f"https://api.github.com/repos/{REPO}/releases/assets/{asset_id}"
headers = {
'Authorization': f"Bearer {token}",
'Accept': 'application/octet-stream',
'X-GitHub-Api-Version': '2022-11-28'
}
# Download to temp file
tmpf = tempfile.NamedTemporaryFile(delete=False)
try:
with requests.get(download_url, headers=headers, stream=True) as r:
r.raise_for_status()
for chunk in r.iter_content(chunk_size=8192):
tmpf.write(chunk)
tmpf.close()
proc = subprocess.run(
['dart', 'run', 'auto_updater:sign_update', tmpf.name],
check=True, capture_output=True
)
out = proc.stdout.decode().strip()
m = signature_re.search(out)
if not m:
raise RuntimeError(f"Failed to parse signature from: {out}")
sig_value = m.group(1)
size = asset.get('size') or os.path.getsize(tmpf.name)
# Add enclosure
ET.SubElement(item, 'enclosure', {
'url': f"https://github.com/{REPO}/releases/download/v{tag}/{asset['name']}",
'sparkle:edSignature': sig_value,
'sparkle:os': os_name,
'length': str(size),
'type': 'application/octet-stream'
})
finally:
os.unlink(tmpf.name)
indent(rss)
tree = ET.ElementTree(rss)
os.makedirs(os.path.dirname(OUT_PATH), exist_ok=True)
tree.write(OUT_PATH, encoding='utf-8', xml_declaration=True)
print(f"Generated {OUT_PATH}")
if __name__ == '__main__':
main()