updates versioning script

This commit is contained in:
Jeffrey Picard 2021-07-14 17:42:12 -04:00
parent e6cd5bc220
commit 6b45d7470e

View file

@ -1,279 +1,318 @@
import os import argparse
import io import io
import sys import json
import json import os
import argparse import sys
import unittest import unittest
from datetime import date, datetime from datetime import date, datetime
from getpass import getpass from getpass import getpass
try: try:
import github3 import github3
except ImportError: except ImportError:
print('To run release tool you need to install github3.py:') print('To run release tool you need to install github3.py:')
print('') print('')
print(' $ pip install github3.py') print(' $ pip install github3.py')
print('') print('')
sys.exit(1) sys.exit(1)
AREA_RENAME = { AREA_RENAME = {
'api': 'API', 'api': 'API',
'dht': 'DHT' 'dht': 'DHT'
} }
def get_github(): def get_github():
config_path = os.path.expanduser('~/.lbry-release-tool.json') config_path = os.path.expanduser('~/.lbry-release-tool.json')
if os.path.exists(config_path): if os.path.exists(config_path):
with open(config_path, 'r') as config_file: with open(config_path, 'r') as config_file:
config = json.load(config_file) config = json.load(config_file)
return github3.github.GitHub(token=config['token']) return github3.github.GitHub(token=config['token'])
token = os.environ.get("GH_TOKEN") token = os.environ.get("GH_TOKEN")
if not token: if not token:
print('GitHub Credentials') print('GitHub Credentials')
username = input('username: ') username = input('username: ')
password = getpass('password: ') password = getpass('password: ')
gh = github3.github.GitHub(username, password) gh = github3.github.GitHub(username, password)
token = input('Enter 2FA: ') token = input('Enter 2FA: ')
with open(config_path, 'w') as config_file: with open(config_path, 'w') as config_file:
json.dump({'token': token}, config_file) json.dump({'token': token}, config_file)
gh.login(token=token) gh.login(token=token)
return gh return gh
def get_labels(pr, prefix): def get_labels(pr, prefix):
for label in pr.labels: for label in pr.labels:
label_name = label['name'] label_name = label['name']
if label_name.startswith(f'{prefix}: '): if label_name.startswith(f'{prefix}: '):
yield label_name[len(f'{prefix}: '):] yield label_name[len(f'{prefix}: '):]
def get_label(pr, prefix): def get_label(pr, prefix):
for label in get_labels(pr, prefix): for label in get_labels(pr, prefix):
return label return label
BACKWARDS_INCOMPATIBLE = 'backwards-incompatible:' BACKWARDS_INCOMPATIBLE = 'backwards-incompatible:'
RELEASE_TEXT = 'release-text:' RELEASE_TEXT = 'release-text:'
RELEASE_TEXT_LINES = 'release-text-lines:' RELEASE_TEXT_LINES = 'release-text-lines:'
def get_backwards_incompatible(desc: str): def get_backwards_incompatible(desc: str):
for line in desc.splitlines(): for line in desc.splitlines():
if line.startswith(BACKWARDS_INCOMPATIBLE): if line.startswith(BACKWARDS_INCOMPATIBLE):
yield line[len(BACKWARDS_INCOMPATIBLE):] yield line[len(BACKWARDS_INCOMPATIBLE):]
def get_release_text(desc: str): def get_release_text(desc: str):
in_release_lines = False in_release_lines = False
for line in desc.splitlines(): for line in desc.splitlines():
if in_release_lines: if in_release_lines:
yield line.rstrip() yield line.rstrip()
elif line.startswith(RELEASE_TEXT_LINES): elif line.startswith(RELEASE_TEXT_LINES):
in_release_lines = True in_release_lines = True
elif line.startswith(RELEASE_TEXT): elif line.startswith(RELEASE_TEXT):
yield line[len(RELEASE_TEXT):].strip() yield line[len(RELEASE_TEXT):].strip()
yield '' yield ''
class Version: class Version:
def __init__(self, major=0, date=datetime.now(), micro=0, alphabeta=""): def __init__(self, major=0, date=datetime.now(), micro=0, alphabeta=""):
self.major = int(major) self.major = int(major)
self.date = date self.date = date
self.micro = int(micro) self.micro = int(micro)
self.alphabeta = alphabeta self.alphabeta = alphabeta
@classmethod @classmethod
def from_string(cls, version_string): def from_string(cls, version_string):
(major, datemicroalphabeta) = version_string.split('.', 1) (major, datemicroalphabeta) = version_string.split('.', 1)
parts = datemicroalphabeta.split("-") parts = datemicroalphabeta.split("-")
if len(parts) > 1: if len(parts) > 1:
datemicro, alphabeta = parts[0], parts[1] datemicro, alphabeta = parts[0], parts[1]
else: else:
datemicro, alphabeta = parts[0], "" datemicro, alphabeta = parts[0], ""
date, micro = datemicro.rsplit('.', 1) if datemicro.count('.') > 2 else (datemicro, "0") date, micro = datemicro.rsplit('.', 1) if datemicro.count('.') > 2 else (datemicro, "0")
return cls(major.replace("v", ""), datetime.strptime(date, "%Y.%m.%d"), int(micro), alphabeta) return cls(major.replace("v", ""), datetime.strptime(date, "%Y.%m.%d"), int(micro), alphabeta)
@classmethod @classmethod
def from_content(cls, content): def from_content(cls, content):
src = content.decoded.decode('utf-8') src = content.decoded.decode('utf-8')
return cls.from_string(src) return cls.from_string(src)
def increment(self, action): def increment(self, action):
cls = self.__class__ cls = self.__class__
if action == 'major': if action == 'major':
return cls(self.major+1, datetime.now(), self.micro, self.alphabeta) return cls(self.major+1, datetime.now(), self.micro, self.alphabeta)
elif action == 'date': elif action == 'date':
return cls(self.major, datetime.now(), self.micro, self.alphabeta) return cls(self.major, datetime.now(), self.micro, self.alphabeta)
elif action == 'micro': elif action == 'micro':
return cls(self.major, datetime.now(), self.micro+1, self.alphabeta) return cls(self.major, datetime.now(), self.micro+1, self.alphabeta)
raise ValueError(f'unknown action: {action}') raise ValueError(f'unknown action: {action}')
@property @property
def tag(self): def tag(self):
return f'{self}' return f'{self}'
def __str__(self): def __str__(self):
arr = ['v', self.major, '.', self.date.strftime("%Y.%m.%d")] arr = ['v', self.major, '.', self.date.strftime("%Y.%m.%d")]
if self.micro > 0: if self.micro > 0:
arr += [".", self.micro] arr += [".", self.micro]
if self.alphabeta != "": if self.alphabeta != "":
arr += ["-", self.alphabeta] arr += ["-", self.alphabeta]
return ''.join(str(p) for p in arr) return ''.join(str(p) for p in arr)
def release(args): def release(args):
gh = get_github() gh = get_github()
repo = gh.repository('lbryio', 'hub') repo = gh.repository('lbryio', 'hub')
try: try:
version_file = repo.file_contents('version.txt') version_file = repo.file_contents('version.txt')
current_version = Version.from_content(version_file) current_version = Version.from_content(version_file)
print(f'Current Version: {current_version}') print(f'Current Version: {current_version}')
except: except:
current_version = Version(0) current_version = Version()
version_file = repo.create_file("version.txt", message="add version file", content=str(current_version).encode('utf-8')) version_file = repo.create_file("version.txt", message="add version file", content=str(current_version).encode('utf-8'))
if not args.confirm: if not args.confirm:
print("\nDRY RUN ONLY. RUN WITH --confirm TO DO A REAL RELEASE.\n") print("\nDRY RUN ONLY. RUN WITH --confirm TO DO A REAL RELEASE.\n")
if args.action == 'current': if args.action == 'current':
new_version = current_version new_version = current_version
else: else:
new_version = current_version.increment(args.action) new_version = current_version.increment(args.action)
print(f' New Version: {new_version}') print(f' New Version: {new_version}')
previous_release = repo.release_from_tag(args.start_tag or current_version.tag) tag = args.start_tag if args.start_tag else current_version.tag
print(f' Changelog From: {previous_release.tag_name} ({previous_release.created_at})') try:
print() previous_release = repo.release_from_tag(tag)
except github3.exceptions.NotFoundError:
incompats = [] previous_release = list(repo.releases())[-1]
release_texts = [] print(f' Changelog From: {previous_release.tag_name} ({previous_release.created_at})')
unlabeled = [] print()
fixups = []
areas = {} incompats = []
for pr in gh.search_issues(f"merged:>={previous_release._json_data['created_at']} repo:lbryio/hub"): release_texts = []
area_labels = list(get_labels(pr, 'area')) unlabeled = []
type_label = get_label(pr, 'type') fixups = []
if area_labels and type_label: areas = {}
for area_name in area_labels: for pr in gh.search_issues(f"merged:>={previous_release._json_data['created_at']} repo:lbryio/hub"):
for incompat in get_backwards_incompatible(pr.body or ""): area_labels = list(get_labels(pr, 'area'))
incompats.append(f' * [{area_name}] {incompat.strip()} ({pr.html_url})') type_label = get_label(pr, 'type')
for release_text in get_release_text(pr.body or ""): if area_labels and type_label:
release_texts.append(release_text) for area_name in area_labels:
if type_label == 'fixup': for incompat in get_backwards_incompatible(pr.body or ""):
fixups.append(f' * {pr.title} ({pr.html_url}) by {pr.user["login"]}') incompats.append(f' * [{area_name}] {incompat.strip()} ({pr.html_url})')
else: for release_text in get_release_text(pr.body or ""):
area = areas.setdefault(area_name, []) release_texts.append(release_text)
area.append(f' * [{type_label}] {pr.title} ({pr.html_url}) by {pr.user["login"]}') if type_label == 'fixup':
else: fixups.append(f' * {pr.title} ({pr.html_url}) by {pr.user["login"]}')
unlabeled.append(f' * {pr.title} ({pr.html_url}) by {pr.user["login"]}') else:
area = areas.setdefault(area_name, [])
area_names = list(areas.keys()) area.append(f' * [{type_label}] {pr.title} ({pr.html_url}) by {pr.user["login"]}')
area_names.sort() else:
unlabeled.append(f' * {pr.title} ({pr.html_url}) by {pr.user["login"]}')
body = io.StringIO()
w = lambda s: body.write(s+'\n') area_names = list(areas.keys())
area_names.sort()
w(f'## [{new_version}] - {date.today().isoformat()}')
if release_texts: body = io.StringIO()
w('') w = lambda s: body.write(s+'\n')
for release_text in release_texts:
w(release_text) w(f'## [{new_version}] - {date.today().isoformat()}')
if incompats: if release_texts:
w('') w('')
w(f'### Backwards Incompatible Changes') for release_text in release_texts:
for incompat in incompats: w(release_text)
w(incompat) if incompats:
for area in area_names: w('')
prs = areas[area] w(f'### Backwards Incompatible Changes')
area = AREA_RENAME.get(area.lower(), area.capitalize()) for incompat in incompats:
w('') w(incompat)
w(f'### {area}') for area in area_names:
for pr in prs: prs = areas[area]
w(pr) area = AREA_RENAME.get(area.lower(), area.capitalize())
w('')
print(body.getvalue()) w(f'### {area}')
for pr in prs:
if unlabeled: w(pr)
print('The following PRs were skipped and not included in changelog:')
for skipped in unlabeled: print(body.getvalue())
print(skipped)
if unlabeled:
if fixups: print('The following PRs were skipped and not included in changelog:')
print('The following PRs were marked as fixups and not included in changelog:') for skipped in unlabeled:
for skipped in fixups: print(skipped)
print(skipped)
if fixups:
if args.confirm: print('The following PRs were marked as fixups and not included in changelog:')
for skipped in fixups:
commit = version_file.update( print(skipped)
new_version.tag,
version_file.decoded.decode('utf-8').replace(str(current_version), str(new_version)).encode() if args.confirm:
)['commit']
commit = version_file.update(
repo.create_tag( new_version.tag,
tag=new_version.tag, version_file.decoded.decode('utf-8').replace(str(current_version), str(new_version)).encode()
message=new_version.tag, )['commit']
sha=commit.sha,
obj_type='commit', if args.action != "current":
tagger=commit.committer repo.create_tag(
) tag=new_version.tag,
message=new_version.tag,
repo.create_release( sha=commit.sha,
new_version.tag, obj_type='commit',
name=new_version.tag, tagger=commit.committer
body=body.getvalue(), )
draft=True,
) repo.create_release(
new_version.tag,
return 0 name=new_version.tag,
body=body.getvalue(),
draft=True,
class TestReleaseTool(unittest.TestCase): )
elif args.action == "current":
def test_version_parsing(self): print("in args.action == current")
self.assertTrue(str(Version.from_string('v1.2020.01.01-beta')), 'v1.2020.01.01-beta') try:
self.assertTrue(str(Version.from_string('v1.2020.01.01.10')), 'v1.2020.01.01-beta') print(new_version.tag)
# if we have the tag and release already don't do anything
def test_version_increment(self): release = repo.release_from_tag(new_version.tag)
v = Version.from_string('v1.2020.01.01-beta') if release.prerelease:
self.assertTrue(str(v.increment('major')), 'v2.2020.01.01.beta') release.edit(prerelease=False)
self.assertTrue(str(v.increment('date')), f'v1.{datetime.now().strftime("YYYY.MM.dd")}-beta') return
except Exception as e:
print(e)
def test(): try:
runner = unittest.TextTestRunner(verbosity=2) # We need to do this to get draft and prerelease releases
loader = unittest.TestLoader() release = repo.releases().next()
suite = loader.loadTestsFromTestCase(TestReleaseTool) # Case me have a release and no tag
return 0 if runner.run(suite).wasSuccessful() else 1 if release.name == new_version.tag:
if release.draft:
release.edit(draft=False, prerelease=True)
def main(): elif release.prerelease:
parser = argparse.ArgumentParser() release.edit(prerelease=False)
parser.add_argument("--confirm", default=False, action="store_true", return
help="without this flag, it will only print what it will do but will not actually do it") else:
parser.add_argument("--start-tag", help="custom starting tag for changelog generation") raise Exception("asdf")
parser.add_argument("action", choices=['test', 'current', 'major', 'minor', 'micro']) except:
args = parser.parse_args() repo.create_tag(
tag=new_version.tag,
if args.action == "test": message=new_version.tag,
code = test() sha=commit.sha,
else: obj_type='commit',
code = release(args) tagger=commit.committer
)
print() repo.create_release(
return code new_version.tag,
name=new_version.tag,
body=body.getvalue(),
if __name__ == "__main__": draft=True,
sys.exit(main()) )
class TestReleaseTool(unittest.TestCase):
def test_version_parsing(self):
self.assertTrue(str(Version.from_string('v1.2020.01.01-beta')), 'v1.2020.01.01-beta')
self.assertTrue(str(Version.from_string('v1.2020.01.01.10')), 'v1.2020.01.01-beta')
def test_version_increment(self):
v = Version.from_string('v1.2020.01.01-beta')
self.assertTrue(str(v.increment('major')), 'v2.2020.01.01.beta')
self.assertTrue(str(v.increment('date')), f'v1.{datetime.now().strftime("YYYY.MM.dd")}-beta')
def test():
runner = unittest.TextTestRunner(verbosity=2)
loader = unittest.TestLoader()
suite = loader.loadTestsFromTestCase(TestReleaseTool)
return 0 if runner.run(suite).wasSuccessful() else 1
def main():
parser = argparse.ArgumentParser()
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()
if args.action == "test":
code = test()
else:
code = release(args)
print()
return code
if __name__ == "__main__":
sys.exit(main())