lbry-sdk/scripts/release.py

268 lines
7.8 KiB
Python
Raw Normal View History

import os
import re
import io
import sys
import json
import argparse
import unittest
from datetime import date
from getpass import getpass
try:
import github3
except ImportError:
print('To run release tool you need to install github3.py:')
print('')
print(' $ pip install github3.py')
print('')
sys.exit(1)
AREA_RENAME = {
'api': 'API',
'dht': 'DHT'
}
def get_github():
config_path = os.path.expanduser('~/.lbry-release-tool.json')
if os.path.exists(config_path):
with open(config_path, 'r') as config_file:
config = json.load(config_file)
return github3.login(token=config['token'])
2019-02-05 15:58:42 +01:00
token = os.environ.get("GH_TOKEN")
if not token:
print('GitHub Credentials')
username = input('username: ')
password = getpass('password: ')
gh = github3.authorize(
username, password, ['repo'], 'lbry release tool',
two_factor_callback=lambda: input('Enter 2FA: ')
)
with open(config_path, 'w') as config_file:
json.dump({'token': gh.token}, config_file)
token = gh.token
return github3.login(token=token)
def get_labels(pr, prefix):
for label in pr.labels:
label_name = label['name']
if label_name.startswith(f'{prefix}: '):
yield label_name[len(f'{prefix}: '):]
def get_label(pr, prefix):
for label in get_labels(pr, prefix):
return label
BACKWARDS_INCOMPATIBLE = 'backwards-incompatible:'
RELEASE_TEXT = 'release-text:'
RELEASE_TEXT_LINES = 'release-text-lines:'
def get_backwards_incompatible(desc: str):
for line in desc.splitlines():
if line.startswith(BACKWARDS_INCOMPATIBLE):
yield line[len(BACKWARDS_INCOMPATIBLE):]
2019-02-05 15:58:42 +01:00
def get_release_text(desc: str):
in_release_lines = False
2019-02-05 15:58:42 +01:00
for line in desc.splitlines():
if in_release_lines:
yield line.rstrip()
elif line.startswith(RELEASE_TEXT_LINES):
in_release_lines = True
elif line.startswith(RELEASE_TEXT):
yield line[len(RELEASE_TEXT):].strip()
yield ''
2019-02-05 15:58:42 +01:00
class Version:
2019-07-16 19:02:15 +02:00
def __init__(self, major=0, minor=0, micro=0):
self.major = int(major)
self.minor = int(minor)
self.micro = int(micro)
@classmethod
def from_string(cls, version_string):
(major, minor, micro), rc = version_string.split('.'), None
if 'rc' in micro:
micro, rc = micro.split('rc')
2019-07-16 19:02:15 +02:00
return cls(major, minor, micro)
@classmethod
def from_content(cls, content):
src = content.decoded.decode('utf-8')
version = re.search('__version__ = "(.*?)"', src).group(1)
return cls.from_string(version)
def increment(self, action):
cls = self.__class__
2019-07-16 19:02:15 +02:00
if action == 'major':
return cls(self.major+1)
elif action == 'minor':
return cls(self.major, self.minor+1)
elif action == 'micro':
return cls(self.major, self.minor, self.micro+1)
raise ValueError(f'unknown action: {action}')
@property
def tag(self):
return f'v{self}'
def __str__(self):
2019-07-16 19:02:15 +02:00
return '.'.join(str(p) for p in [self.major, self.minor, self.micro])
def release(args):
gh = get_github()
2019-06-21 05:13:57 +02:00
repo = gh.repository('lbryio', 'lbry-sdk')
version_file = repo.file_contents('lbry/__init__.py')
2019-07-16 19:02:15 +02:00
if not args.confirm:
print("\nDRY RUN ONLY. RUN WITH --confirm TO DO A REAL RELEASE.\n")
current_version = Version.from_content(version_file)
print(f'Current Version: {current_version}')
if args.action == 'current':
new_version = current_version
else:
new_version = current_version.increment(args.action)
print(f' New Version: {new_version}')
previous_release = repo.release_from_tag(args.start_tag or current_version.tag)
print(f' Changelog From: {previous_release.tag_name} ({previous_release.created_at})')
print()
incompats = []
2019-02-05 15:58:42 +01:00
release_texts = []
unlabeled = []
fixups = []
areas = {}
2019-06-21 05:13:57 +02:00
for pr in gh.search_issues(f"merged:>={previous_release._json_data['created_at']} repo:lbryio/lbry-sdk"):
area_labels = list(get_labels(pr, 'area'))
type_label = get_label(pr, 'type')
if area_labels and type_label:
for area_name in area_labels:
for incompat in get_backwards_incompatible(pr.body or ""):
incompats.append(f' * [{area_name}] {incompat.strip()} ({pr.html_url})')
for release_text in get_release_text(pr.body or ""):
release_texts.append(release_text)
if type_label == 'fixup':
fixups.append(f' * {pr.title} ({pr.html_url}) by {pr.user["login"]}')
else:
area = areas.setdefault(area_name, [])
area.append(f' * [{type_label}] {pr.title} ({pr.html_url}) by {pr.user["login"]}')
else:
unlabeled.append(f' * {pr.title} ({pr.html_url}) by {pr.user["login"]}')
area_names = list(areas.keys())
area_names.sort()
body = io.StringIO()
w = lambda s: body.write(s+'\n')
w(f'## [{new_version}] - {date.today().isoformat()}')
2019-02-05 15:58:42 +01:00
if release_texts:
w('')
for release_text in release_texts:
w(release_text)
if incompats:
w('')
w(f'### Backwards Incompatible Changes')
for incompat in incompats:
w(incompat)
for area in area_names:
prs = areas[area]
area = AREA_RENAME.get(area.lower(), area.capitalize())
w('')
w(f'### {area}')
for pr in prs:
w(pr)
print(body.getvalue())
if unlabeled:
print('The following PRs were skipped and not included in changelog:')
for skipped in unlabeled:
print(skipped)
if fixups:
print('The following PRs were marked as fixups and not included in changelog:')
for skipped in fixups:
print(skipped)
2019-07-16 19:02:15 +02:00
if args.confirm:
commit = version_file.update(
new_version.tag,
version_file.decoded.decode('utf-8').replace(str(current_version), str(new_version)).encode()
)['commit']
repo.create_tag(
tag=new_version.tag,
message=new_version.tag,
sha=commit.sha,
obj_type='commit',
tagger=commit.committer
)
repo.create_release(
new_version.tag,
name=new_version.tag,
body=body.getvalue(),
draft=True,
)
2019-07-16 19:02:15 +02:00
return 0
class TestReleaseTool(unittest.TestCase):
def test_version_parsing(self):
self.assertTrue(str(Version.from_string('1.2.3')), '1.2.3')
self.assertTrue(str(Version.from_string('1.2.3rc4')), '1.2.3rc4')
def test_version_increment(self):
v = Version.from_string('1.2.3')
2019-07-16 19:02:15 +02:00
self.assertTrue(str(v.increment('major')), '2.0.0')
self.assertTrue(str(v.increment('minor')), '1.3.0')
self.assertTrue(str(v.increment('micro')), '1.2.4')
def test():
runner = unittest.TextTestRunner(verbosity=2)
loader = unittest.TestLoader()
suite = loader.loadTestsFromTestCase(TestReleaseTool)
2019-07-16 19:02:15 +02:00
return 0 if runner.run(suite).wasSuccessful() else 1
def main():
parser = argparse.ArgumentParser()
2019-07-16 19:02:15 +02:00
parser.add_argument("--confirm", default=False, action="store_true",
help="without this flag, it will only print what it will do but will not actually do it")
parser.add_argument("--start-tag", help="custom starting tag for changelog generation")
parser.add_argument("action", choices=['test', 'current', 'major', 'minor', 'micro'])
args = parser.parse_args()
2019-07-16 19:02:15 +02:00
if args.action == "test":
code = test()
else:
2019-07-16 19:02:15 +02:00
code = release(args)
print()
return code
if __name__ == "__main__":
2019-07-16 19:02:15 +02:00
sys.exit(main())