Skip to content

Commit b31cd05

Browse files
authored
Merge pull request libgit2#5372 from pks-t/pks/release-script
Release script
2 parents 1256b46 + 2ae45bc commit b31cd05

File tree

2 files changed

+175
-0
lines changed

2 files changed

+175
-0
lines changed

.editorconfig

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@ indent_size = 2
1515
indent_style = space
1616
indent_size = 4
1717
trim_trailing_whitespace = false
18+
19+
[*.py]
20+
indent_style = space
21+
indent_size = 4

script/release.py

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
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)

0 commit comments

Comments
 (0)