release.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. import os, sys, requests, pprint, re, json
  2. from uritemplate import URITemplate, expand
  3. from subprocess import call, Popen, PIPE
  4. from os.path import expanduser
  5. changelog_file = '../../changelog.txt'
  6. token_file = '../../../DesktopPrivate/github-releases-token.txt'
  7. version = ''
  8. commit = ''
  9. for arg in sys.argv:
  10. if re.match(r'\d+\.\d+', arg):
  11. version = arg
  12. elif re.match(r'^[a-f0-9]{40}$', arg):
  13. commit = arg
  14. # thanks http://stackoverflow.com/questions/13909900/progress-of-python-requests-post
  15. class upload_in_chunks(object):
  16. def __init__(self, filename, chunksize=1 << 13):
  17. self.filename = filename
  18. self.chunksize = chunksize
  19. self.totalsize = os.path.getsize(filename)
  20. self.readsofar = 0
  21. def __iter__(self):
  22. with open(self.filename, 'rb') as file:
  23. while True:
  24. data = file.read(self.chunksize)
  25. if not data:
  26. sys.stderr.write("\n")
  27. break
  28. self.readsofar += len(data)
  29. percent = self.readsofar * 1e2 / self.totalsize
  30. sys.stderr.write("\r{percent:3.0f}%".format(percent=percent))
  31. yield data
  32. def __len__(self):
  33. return self.totalsize
  34. class IterableToFileAdapter(object):
  35. def __init__(self, iterable):
  36. self.iterator = iter(iterable)
  37. self.length = len(iterable)
  38. def read(self, size=-1): # TBD: add buffer for `len(data) > size` case
  39. return next(self.iterator, b'')
  40. def __len__(self):
  41. return self.length
  42. def checkResponseCode(result, right_code):
  43. if (result.status_code != right_code):
  44. print('Wrong result code: ' + str(result.status_code) + ', should be ' + str(right_code))
  45. sys.exit(1)
  46. def getOutput(command):
  47. p = Popen(command.split(), stdout=PIPE)
  48. output, err = p.communicate()
  49. if err != None or p.returncode != 0:
  50. print('ERROR!')
  51. print(err)
  52. print(p.returncode)
  53. sys.exit(1)
  54. return output.decode('utf-8')
  55. def invoke(command):
  56. return call(command.split()) == 0
  57. def appendSubmodules(appendTo, root, rootRevision):
  58. startpath = os.getcwd()
  59. lines = getOutput('git submodule foreach').split('\n')
  60. for line in lines:
  61. if len(line) == 0:
  62. continue
  63. match = re.match(r"^Entering '([^']+)'$", line)
  64. if not match:
  65. print('Bad line: ' + line)
  66. return False
  67. path = match.group(1)
  68. subroot = root + '/' + path
  69. revision = getOutput('git rev-parse ' + rootRevision + ':' + path).split('\n')[0]
  70. print('Adding submodule ' + path + '...')
  71. os.chdir(path)
  72. tmppath = appendTo + '_tmp'
  73. if not invoke('git archive --prefix=' + subroot + '/ ' + revision + ' -o ' + tmppath + '.tar'):
  74. os.remove(appendTo + '.tar')
  75. os.remove(tmppath + '.tar')
  76. return False
  77. if not appendSubmodules(tmppath, subroot, revision):
  78. return False
  79. tar = 'tar' if sys.platform == 'linux' else 'gtar'
  80. if not invoke(tar + ' --concatenate --file=' + appendTo + '.tar ' + tmppath + '.tar'):
  81. os.remove(appendTo + '.tar')
  82. os.remove(tmppath + '.tar')
  83. return False
  84. os.remove(tmppath + '.tar')
  85. os.chdir(startpath)
  86. return True
  87. def prepareSources():
  88. workpath = os.getcwd()
  89. os.chdir('../..')
  90. rootpath = os.getcwd()
  91. finalpart = rootpath + '/out/Release/sources'
  92. finalpath = finalpart + '.tar'
  93. if os.path.exists(finalpath):
  94. os.remove(finalpath)
  95. if os.path.exists(finalpath + '.gz'):
  96. os.remove(finalpath + '.gz')
  97. tmppath = rootpath + '/out/Release/tmp.tar'
  98. print('Preparing source tarball...')
  99. revision = 'v' + version
  100. targetRoot = 'tdesktop-' + version + '-full';
  101. if not invoke('git archive --prefix=' + targetRoot + '/ -o ' + finalpath + ' ' + revision):
  102. os.remove(finalpath)
  103. sys.exit(1)
  104. if not appendSubmodules(finalpart, targetRoot, revision):
  105. sys.exit(1)
  106. print('Compressing...')
  107. if not invoke('gzip -9 ' + finalpath):
  108. os.remove(finalpath)
  109. sys.exit(1)
  110. os.chdir(workpath)
  111. return finalpath + '.gz'
  112. pp = pprint.PrettyPrinter(indent=2)
  113. url = 'https://api.github.com/'
  114. version_parts = version.split('.')
  115. stable = 1
  116. beta = 0
  117. if len(version_parts) < 2:
  118. print('Error: expected at least major version ' + version)
  119. sys.exit(1)
  120. if len(version_parts) > 4:
  121. print('Error: bad version passed ' + version)
  122. sys.exit(1)
  123. version_major = version_parts[0] + '.' + version_parts[1]
  124. if len(version_parts) == 2:
  125. version = version_major + '.0'
  126. version_full = version
  127. else:
  128. version = version_major + '.' + version_parts[2]
  129. version_full = version
  130. if len(version_parts) == 4:
  131. if version_parts[3] == 'beta':
  132. beta = 1
  133. stable = 0
  134. version_full = version + '.beta'
  135. else:
  136. print('Error: unexpected version part ' + version_parts[3])
  137. sys.exit(1)
  138. access_token = ''
  139. if os.path.isfile(token_file):
  140. with open(token_file) as f:
  141. for line in f:
  142. access_token = line.replace('\n', '')
  143. if access_token == '':
  144. print('Access token not found!')
  145. sys.exit(1)
  146. print('Version: ' + version_full)
  147. local_base = expanduser("~") + '/Projects/backup/tdesktop'
  148. if not os.path.isdir(local_base):
  149. local_base = '/mnt/c/Telegram/Projects/backup/tdesktop'
  150. if not os.path.isdir(local_base):
  151. print('Backup path not found: ' + local_base)
  152. sys.exit(1)
  153. local_folder = local_base + '/' + version_major + '/' + version_full
  154. if stable == 1:
  155. if os.path.isdir(local_folder + '.beta'):
  156. beta = 1
  157. stable = 0
  158. version_full = version + '.beta'
  159. local_folder = local_folder + '.beta'
  160. if not os.path.isdir(local_folder):
  161. print('Storage path not found: ' + local_folder)
  162. sys.exit(1)
  163. local_folder = local_folder + '/'
  164. files = []
  165. files.append({
  166. 'local': 'sources',
  167. 'remote': 'tdesktop-' + version + '-full.tar.gz',
  168. 'mime': 'application/x-gzip',
  169. 'label': 'Source code (tar.gz, full)',
  170. })
  171. files.append({
  172. 'local': 'tsetup.' + version_full + '.exe',
  173. 'remote': 'tsetup.' + version_full + '.exe',
  174. 'backup_folder': 'tsetup',
  175. 'mime': 'application/octet-stream',
  176. 'label': 'Windows 32 bit: Installer',
  177. })
  178. files.append({
  179. 'local': 'tportable.' + version_full + '.zip',
  180. 'remote': 'tportable.' + version_full + '.zip',
  181. 'backup_folder': 'tsetup',
  182. 'mime': 'application/zip',
  183. 'label': 'Windows 32 bit: Portable',
  184. })
  185. files.append({
  186. 'local': 'tsetup-x64.' + version_full + '.exe',
  187. 'remote': 'tsetup-x64.' + version_full + '.exe',
  188. 'backup_folder': 'tx64',
  189. 'mime': 'application/octet-stream',
  190. 'label': 'Windows 64 bit: Installer',
  191. })
  192. files.append({
  193. 'local': 'tportable-x64.' + version_full + '.zip',
  194. 'remote': 'tportable-x64.' + version_full + '.zip',
  195. 'backup_folder': 'tx64',
  196. 'mime': 'application/zip',
  197. 'label': 'Windows 64 bit: Portable',
  198. })
  199. files.append({
  200. 'local': 'tsetup-arm64.' + version_full + '.exe',
  201. 'remote': 'tsetup-arm64.' + version_full + '.exe',
  202. 'backup_folder': 'tarm64',
  203. 'mime': 'application/octet-stream',
  204. 'label': 'Windows on ARM: Installer',
  205. })
  206. files.append({
  207. 'local': 'tportable-arm64.' + version_full + '.zip',
  208. 'remote': 'tportable-arm64.' + version_full + '.zip',
  209. 'backup_folder': 'tarm64',
  210. 'mime': 'application/zip',
  211. 'label': 'Windows on ARM: Portable',
  212. })
  213. files.append({
  214. 'local': 'tsetup.' + version_full + '.dmg',
  215. 'remote': 'tsetup.' + version_full + '.dmg',
  216. 'backup_folder': 'tmac',
  217. 'mime': 'application/octet-stream',
  218. 'label': 'macOS 10.13+: Installer',
  219. })
  220. files.append({
  221. 'local': 'tsetup.' + version_full + '.tar.xz',
  222. 'remote': 'tsetup.' + version_full + '.tar.xz',
  223. 'backup_folder': 'tlinux',
  224. 'mime': 'application/octet-stream',
  225. 'label': 'Linux 64 bit: Binary',
  226. })
  227. r = requests.get(url + 'repos/telegramdesktop/tdesktop/releases/tags/v' + version)
  228. if r.status_code == 404:
  229. print('Release not found, creating.')
  230. if commit == '':
  231. print('Error: specify the commit.')
  232. sys.exit(1)
  233. if not os.path.isfile(changelog_file):
  234. print('Error: Changelog file not found.')
  235. sys.exit(1)
  236. changelog = ''
  237. started = 0
  238. with open(changelog_file) as f:
  239. for line in f:
  240. if started == 1:
  241. if re.match(r'^\d+\.\d+', line):
  242. break
  243. changelog += line
  244. else:
  245. if re.match(r'^\d+\.\d+', line):
  246. if line[0:len(version) + 1] == version + ' ':
  247. started = 1
  248. elif line[0:len(version_major) + 1] == version_major + ' ':
  249. if version_major + '.0' == version:
  250. started = 1
  251. if started != 1:
  252. print('Error: Changelog not found.')
  253. sys.exit(1)
  254. changelog = changelog.strip()
  255. print('Changelog: ')
  256. print(changelog)
  257. r = requests.post(url + 'repos/telegramdesktop/tdesktop/releases', headers={'Authorization': 'token ' + access_token}, data=json.dumps({
  258. 'tag_name': 'v' + version,
  259. 'target_commitish': commit,
  260. 'name': 'v ' + version,
  261. 'body': changelog,
  262. 'prerelease': (beta == 1),
  263. }))
  264. checkResponseCode(r, 201)
  265. tagname = 'v' + version
  266. invoke("git fetch origin")
  267. if stable == 1:
  268. invoke("git push launchpad {}:master".format(tagname))
  269. else:
  270. invoke("git push launchpad {}:beta".format(tagname))
  271. invoke("git push --tags launchpad")
  272. r = requests.get(url + 'repos/telegramdesktop/tdesktop/releases/tags/v' + version)
  273. checkResponseCode(r, 200)
  274. release_data = r.json()
  275. #pp.pprint(release_data)
  276. release_id = release_data['id']
  277. print('Release ID: ' + str(release_id))
  278. r = requests.get(url + 'repos/telegramdesktop/tdesktop/releases/' + str(release_id) + '/assets')
  279. checkResponseCode(r, 200)
  280. assets = release_data['assets']
  281. for asset in assets:
  282. name = asset['name']
  283. found = 0
  284. for file in files:
  285. if file['remote'] == name:
  286. print('Already uploaded: ' + name)
  287. file['already'] = 1
  288. found = 1
  289. break
  290. if found == 0:
  291. print('Warning: strange asset: ' + name)
  292. for file in files:
  293. if 'already' in file:
  294. continue
  295. if file['local'] == 'sources':
  296. file_path = prepareSources()
  297. else:
  298. file_path = local_folder + file['backup_folder'] + '/' + file['local']
  299. if not os.path.isfile(file_path):
  300. print('Warning: file not found ' + file['local'])
  301. continue
  302. upload_url = expand(release_data['upload_url'], {'name': file['remote'], 'label': file['label']})
  303. content = upload_in_chunks(file_path, 10)
  304. print('Uploading: ' + file['remote'] + ' (' + str(round(len(content) / 10000) / 100.) + ' MB)')
  305. r = requests.post(upload_url, headers={'Content-Type': file['mime'], 'Authorization': 'token ' + access_token}, data=IterableToFileAdapter(content))
  306. checkResponseCode(r, 201)
  307. print('Success! Removing.')
  308. if not invoke('rm ' + file_path):
  309. print('Bad rm return code :(')
  310. sys.exit(1)
  311. sys.exit()