]>
Commit | Line | Data |
---|---|---|
22a2d3d5 UG |
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 | # Parse CMakeLists | |
44 | with open('CMakeLists.txt') as f: | |
45 | for line in f.readlines(): | |
46 | if line.startswith('project(libgit2 VERSION "{}"'.format(version)): | |
47 | break | |
48 | else: | |
49 | raise Error("cmake: invalid project definition") | |
50 | ||
51 | # Parse version.h | |
52 | with open('include/git2/version.h') as f: | |
53 | lines = f.readlines() | |
54 | ||
55 | for key in expected.keys(): | |
56 | define = '#define LIBGIT2_{} '.format(key) | |
57 | for line in lines: | |
58 | if line.startswith(define): | |
59 | expected[key][1] = line[len(define):].strip() | |
60 | break | |
61 | else: | |
62 | raise Error("version.h: missing define for '{}'".format(key)) | |
63 | ||
64 | for k, v in expected.items(): | |
65 | if v[0] != v[1]: | |
66 | raise Error("version.h: define '{}' does not match (got '{}', expected '{}')".format(k, v[0], v[1])) | |
67 | ||
68 | with open('package.json') as f: | |
69 | pkg = json.load(f) | |
70 | ||
71 | try: | |
72 | pkg_version = Version(pkg["version"]) | |
73 | except KeyError as err: | |
74 | raise Error("package.json: missing the field {}".format(err)) | |
75 | ||
76 | if pkg_version != version: | |
77 | raise Error("package.json: version does not match (got '{}', expected '{}')".format(pkg_version, version)) | |
78 | ||
79 | def generate_relnotes(tree, version): | |
80 | with open('docs/changelog.md') as f: | |
81 | lines = f.readlines() | |
82 | ||
83 | if not lines[0].startswith('v'): | |
84 | raise Error("changelog.md: missing section for v{}".format(version)) | |
85 | try: | |
86 | v = Version(lines[0][1:].strip()) | |
87 | except: | |
88 | raise Error("changelog.md: invalid version string {}".format(lines[0].strip())) | |
89 | if v != version: | |
90 | raise Error("changelog.md: changelog version doesn't match (got {}, expected {})".format(v, version)) | |
91 | if not lines[1].startswith('----'): | |
92 | raise Error("changelog.md: missing version header") | |
93 | if lines[2] != '\n': | |
94 | raise Error("changelog.md: missing newline after version header") | |
95 | ||
96 | for i, line in enumerate(lines[3:]): | |
97 | if not line.startswith('v'): | |
98 | continue | |
99 | try: | |
100 | Version(line[1:].strip()) | |
101 | break | |
102 | except: | |
103 | continue | |
104 | else: | |
105 | raise Error("changelog.md: cannot find section header of preceding release") | |
106 | ||
107 | return ''.join(lines[3:i + 3]).strip() | |
108 | ||
109 | def git(*args): | |
110 | process = subprocess.run([ 'git', *args ], stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
111 | if process.returncode != 0: | |
112 | raise Error('Failed executing git {}: {}'.format(' '.join(args), process.stderr.decode())) | |
113 | return process.stdout | |
114 | ||
115 | def post(url, data, contenttype, user, password): | |
116 | request = urllib.request.Request(url, data=data) | |
117 | request.add_header('Accept', 'application/json') | |
118 | request.add_header('Content-Type', contenttype) | |
119 | request.add_header('Content-Length', len(data)) | |
120 | request.add_header('Authorization', 'Basic ' + base64.b64encode('{}:{}'.format(user, password).encode()).decode()) | |
121 | ||
122 | try: | |
123 | response = urllib.request.urlopen(request) | |
124 | if response.getcode() != 201: | |
125 | raise Error("POST to '{}' failed: {}".format(url, response.reason)) | |
126 | except urllib.error.URLError as e: | |
127 | raise Error("POST to '{}' failed: {}".format(url, e)) | |
128 | data = json.load(response) | |
129 | ||
130 | return data | |
131 | ||
132 | def generate_asset(version, tree, archive_format): | |
133 | Asset = namedtuple('Asset', ['name', 'label', 'mimetype', 'data']) | |
134 | mimetype = 'application/{}'.format('gzip' if archive_format == 'tar.gz' else 'zip') | |
135 | return Asset( | |
136 | "libgit2-{}.{}".format(version, archive_format), "Release sources: libgit2-{}.{}".format(version, archive_format), mimetype, | |
137 | git('archive', '--format', archive_format, '--prefix', 'libgit2-{}/'.format(version), tree) | |
138 | ) | |
139 | ||
140 | def release(args): | |
141 | params = { | |
142 | "tag_name": 'v' + str(args.version), | |
143 | "name": 'libgit2 v' + str(args.version), | |
144 | "target_commitish": git('rev-parse', args.tree).decode().strip(), | |
145 | "body": generate_relnotes(args.tree, args.version), | |
146 | } | |
147 | assets = [ | |
148 | generate_asset(args.version, args.tree, 'tar.gz'), | |
149 | generate_asset(args.version, args.tree, 'zip'), | |
150 | ] | |
151 | ||
152 | if args.dryrun: | |
153 | for k, v in params.items(): | |
154 | print('{}: {}'.format(k, v)) | |
155 | for asset in assets: | |
156 | print('asset: name={}, label={}, mimetype={}, bytes={}'.format(asset.name, asset.label, asset.mimetype, len(asset.data))) | |
157 | return | |
158 | ||
159 | try: | |
160 | url = 'https://api.github.com/repos/{}/releases'.format(args.repository) | |
161 | response = post(url, json.dumps(params).encode(), 'application/json', args.user, args.password) | |
162 | except Error as e: | |
163 | raise Error('Could not create release: ' + str(e)) | |
164 | ||
165 | for asset in assets: | |
166 | try: | |
167 | url = list(urllib.parse.urlparse(response['upload_url'].split('{?')[0])) | |
168 | url[4] = urllib.parse.urlencode({ 'name': asset.name, 'label': asset.label }) | |
169 | post(urllib.parse.urlunparse(url), asset.data, asset.mimetype, args.user, args.password) | |
170 | except Error as e: | |
171 | raise Error('Could not upload asset: ' + str(e)) | |
172 | ||
173 | def main(): | |
174 | parser = argparse.ArgumentParser(description='Create a libgit2 release') | |
175 | parser.add_argument('--tree', default='HEAD', help='tree to create release for (default: HEAD)') | |
176 | parser.add_argument('--dryrun', action='store_true', help='generate release, but do not post it') | |
177 | parser.add_argument('--repository', default='libgit2/libgit2', help='GitHub repository to create repository in') | |
178 | parser.add_argument('--user', help='user to authenticate as') | |
179 | parser.add_argument('--password', help='password to authenticate with') | |
180 | parser.add_argument('version', type=Version, help='version of the new release') | |
181 | args = parser.parse_args() | |
182 | ||
183 | verify_version(args.version) | |
184 | release(args) | |
185 | ||
186 | if __name__ == '__main__': | |
187 | try: | |
188 | main() | |
189 | except Error as e: | |
190 | print(e) | |
191 | sys.exit(1) |