]> git.proxmox.com Git - libgit2.git/blob - script/release.py
e0f29538e7b605598950d79808fb36b09154cc27
[libgit2.git] / script / release.py
1 #!/usr/bin/env python
2
3 from collections import namedtuple
4
5 import argparse
6 import base64
7 import copy
8 import json
9 import subprocess
10 import sys
11 import urllib.parse
12 import urllib.request
13 import urllib.error
14
15 class Error(Exception):
16 pass
17
18 class Version(object):
19 def __init__(self, version):
20 versions = version.split(sep='.')
21 if len(versions) < 2 or len(versions) > 3:
22 raise Error("Invalid version string '{}'".format(version))
23 self.major = int(versions[0])
24 self.minor = int(versions[1])
25 self.revision = int(versions[2]) if len(versions) == 3 else 0
26
27 def __str__(self):
28 return '{}.{}.{}'.format(self.major, self.minor, self.revision)
29
30 def __eq__(self, other):
31 return self.major == other.major and self.minor == other.minor and self.revision == other.revision
32
33 def verify_version(version):
34 expected = {
35 'VERSION': [ '"{}"'.format(version), None ],
36 'VER_MAJOR': [ str(version.major), None ],
37 'VER_MINOR': [ str(version.minor), None ],
38 'VER_REVISION': [ str(version.revision), None ],
39 'VER_PATCH': [ '0', None ],
40 'SOVERSION': [ '"{}.{}"'.format(version.major, version.minor), None ],
41 }
42
43 with open('include/git2/version.h') as f:
44 lines = f.readlines()
45
46 for key in expected.keys():
47 define = '#define LIBGIT2_{} '.format(key)
48 for line in lines:
49 if line.startswith(define):
50 expected[key][1] = line[len(define):].strip()
51 break
52 else:
53 raise Error("version.h: missing define for '{}'".format(key))
54
55 for k, v in expected.items():
56 if v[0] != v[1]:
57 raise Error("version.h: define '{}' does not match (got '{}', expected '{}')".format(k, v[0], v[1]))
58
59 def generate_relnotes(tree, version):
60 with open('docs/changelog.md') as f:
61 lines = f.readlines()
62
63 if not lines[0].startswith('v'):
64 raise Error("changelog.md: missing section for v{}".format(version))
65 try:
66 v = Version(lines[0][1:].strip())
67 except:
68 raise Error("changelog.md: invalid version string {}".format(lines[0].strip()))
69 if v != version:
70 raise Error("changelog.md: changelog version doesn't match (got {}, expected {})".format(v, version))
71 if not lines[1].startswith('----'):
72 raise Error("changelog.md: missing version header")
73 if lines[2] != '\n':
74 raise Error("changelog.md: missing newline after version header")
75
76 for i, line in enumerate(lines[3:]):
77 if not line.startswith('v'):
78 continue
79 try:
80 Version(line[1:].strip())
81 break
82 except:
83 continue
84 else:
85 raise Error("changelog.md: cannot find section header of preceding release")
86
87 return ''.join(lines[3:i + 3]).strip()
88
89 def git(*args):
90 process = subprocess.run([ 'git', *args ], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
91 if process.returncode != 0:
92 raise Error('Failed executing git {}: {}'.format(' '.join(args), process.stderr.decode()))
93 return process.stdout
94
95 def post(url, data, contenttype, user, password):
96 request = urllib.request.Request(url, data=data)
97 request.add_header('Accept', 'application/json')
98 request.add_header('Content-Type', contenttype)
99 request.add_header('Content-Length', len(data))
100 request.add_header('Authorization', 'Basic ' + base64.b64encode('{}:{}'.format(user, password).encode()).decode())
101
102 try:
103 response = urllib.request.urlopen(request)
104 if response.getcode() != 201:
105 raise Error("POST to '{}' failed: {}".format(url, response.reason))
106 except urllib.error.URLError as e:
107 raise Error("POST to '{}' failed: {}".format(url, e))
108 data = json.load(response)
109
110 return data
111
112 def generate_asset(version, tree, archive_format):
113 Asset = namedtuple('Asset', ['name', 'label', 'mimetype', 'data'])
114 mimetype = 'application/{}'.format('gzip' if archive_format == 'tar.gz' else 'zip')
115 return Asset(
116 "libgit2-{}.{}".format(version, archive_format), "Release sources: libgit2-{}.{}".format(version, archive_format), mimetype,
117 git('archive', '--format', archive_format, '--prefix', 'libgit2-{}/'.format(version), tree)
118 )
119
120 def release(args):
121 params = {
122 "tag_name": 'v' + str(args.version),
123 "name": 'libgit2 v' + str(args.version),
124 "target_commitish": git('rev-parse', args.tree).decode().strip(),
125 "body": generate_relnotes(args.tree, args.version),
126 }
127 assets = [
128 generate_asset(args.version, args.tree, 'tar.gz'),
129 generate_asset(args.version, args.tree, 'zip'),
130 ]
131
132 if args.dryrun:
133 for k, v in params.items():
134 print('{}: {}'.format(k, v))
135 for asset in assets:
136 print('asset: name={}, label={}, mimetype={}, bytes={}'.format(asset.name, asset.label, asset.mimetype, len(asset.data)))
137 return
138
139 try:
140 url = 'https://api.github.com/repos/{}/releases'.format(args.repository)
141 response = post(url, json.dumps(params).encode(), 'application/json', args.user, args.password)
142 except Error as e:
143 raise Error('Could not create release: ' + str(e))
144
145 for asset in assets:
146 try:
147 url = list(urllib.parse.urlparse(response['upload_url'].split('{?')[0]))
148 url[4] = urllib.parse.urlencode({ 'name': asset.name, 'label': asset.label })
149 post(urllib.parse.urlunparse(url), asset.data, asset.mimetype, args.user, args.password)
150 except Error as e:
151 raise Error('Could not upload asset: ' + str(e))
152
153 def main():
154 parser = argparse.ArgumentParser(description='Create a libgit2 release')
155 parser.add_argument('--tree', default='HEAD', help='tree to create release for (default: HEAD)')
156 parser.add_argument('--dryrun', action='store_true', help='generate release, but do not post it')
157 parser.add_argument('--repository', default='libgit2/libgit2', help='GitHub repository to create repository in')
158 parser.add_argument('--user', help='user to authenitcate as')
159 parser.add_argument('--password', help='password to authenticate with')
160 parser.add_argument('version', type=Version, help='version of the new release')
161 args = parser.parse_args()
162
163 verify_version(args.version)
164 release(args)
165
166 if __name__ == '__main__':
167 try:
168 main()
169 except Error as e:
170 print(e)
171 sys.exit(1)