Skip to content

Commit 6ee6b51

Browse files
committed
add release
1 parent 9f6fa18 commit 6ee6b51

File tree

3 files changed

+475
-0
lines changed

3 files changed

+475
-0
lines changed

nbdev/release.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/17_release.ipynb.
2+
3+
# %% auto 0
4+
__all__ = ['GH_HOST', 'Release', 'changelog', 'release_git', 'release_gh']
5+
6+
# %% ../nbs/17_release.ipynb 2
7+
from fastcore.all import *
8+
from ghapi.core import *
9+
10+
from datetime import datetime
11+
from configparser import ConfigParser
12+
import shutil,subprocess
13+
14+
# %% ../nbs/17_release.ipynb 4
15+
GH_HOST = "https://api.github.com"
16+
17+
# %% ../nbs/17_release.ipynb 5
18+
def _find_config(cfg_name="settings.ini"):
19+
cfg_path = Path().absolute()
20+
while cfg_path != cfg_path.parent and not (cfg_path/cfg_name).exists(): cfg_path = cfg_path.parent
21+
return Config(cfg_path, cfg_name)
22+
23+
# %% ../nbs/17_release.ipynb 6
24+
def _issue_txt(issue):
25+
res = '- {} ([#{}]({}))'.format(issue.title.strip(), issue.number, issue.html_url)
26+
if hasattr(issue, 'pull_request'): res += ', thanks to [@{}]({})'.format(issue.user.login, issue.user.html_url)
27+
res += '\n'
28+
if not issue.body: return res
29+
return res + f" - {issue.body.strip()}\n"
30+
31+
def _issues_txt(iss, label):
32+
if not iss: return ''
33+
res = f"### {label}\n\n"
34+
return res + '\n'.join(map(_issue_txt, iss))
35+
36+
def _load_json(cfg, k):
37+
try: return json.loads(cfg[k])
38+
except json.JSONDecodeError as e: raise Exception(f"Key: `{k}` in .ini file is not a valid JSON string: {e}")
39+
40+
# %% ../nbs/17_release.ipynb 8
41+
class Release:
42+
def __init__(self, owner=None, repo=None, token=None, **groups):
43+
"Create CHANGELOG.md from GitHub issues"
44+
self.cfg = _find_config()
45+
self.changefile = self.cfg.config_path/'CHANGELOG.md'
46+
if not groups:
47+
default_groups=dict(breaking="Breaking Changes", enhancement="New Features", bug="Bugs Squashed")
48+
groups=_load_json(self.cfg, 'label_groups') if 'label_groups' in self.cfg else default_groups
49+
os.chdir(self.cfg.config_path)
50+
owner,repo = owner or self.cfg.user, repo or self.cfg.lib_name
51+
token = ifnone(token, os.getenv('FASTRELEASE_TOKEN',None))
52+
if not token and Path('token').exists(): token = Path('token').read_text().strip()
53+
if not token: raise Exception('Failed to find token')
54+
self.gh = GhApi(owner, repo, token)
55+
self.groups = groups
56+
57+
def _issues(self, label):
58+
return self.gh.issues.list_for_repo(state='closed', sort='created', filter='all', since=self.commit_date, labels=label)
59+
def _issue_groups(self): return parallel(self._issues, self.groups.keys(), progress=False)
60+
61+
# %% ../nbs/17_release.ipynb 10
62+
@patch
63+
def changelog(self:Release,
64+
debug=False): ## Just print the latest changes, instead of updating file
65+
"Create the CHANGELOG.md file, or return the proposed text if `debug` is `True`"
66+
if not self.changefile.exists(): self.changefile.write_text("# Release notes\n\n<!-- do not remove -->\n")
67+
marker = '<!-- do not remove -->\n'
68+
try: self.commit_date = self.gh.repos.get_latest_release().published_at
69+
except HTTP404NotFoundError: self.commit_date = '2000-01-01T00:00:004Z'
70+
res = f"\n## {self.cfg.version}\n"
71+
issues = self._issue_groups()
72+
res += '\n'.join(_issues_txt(*o) for o in zip(issues, self.groups.values()))
73+
if debug: return res
74+
res = self.changefile.read_text().replace(marker, marker+res+"\n")
75+
shutil.copy(self.changefile, self.changefile.with_suffix(".bak"))
76+
self.changefile.write_text(res)
77+
run(f'git add {self.changefile}')
78+
79+
# %% ../nbs/17_release.ipynb 12
80+
@patch
81+
def release(self:Release):
82+
"Tag and create a release in GitHub for the current version"
83+
ver = self.cfg.version
84+
notes = self.latest_notes()
85+
self.gh.create_release(ver, branch=self.cfg.branch, body=notes)
86+
return ver
87+
88+
# %% ../nbs/17_release.ipynb 14
89+
@patch
90+
def latest_notes(self:Release):
91+
"Latest CHANGELOG entry"
92+
if not self.changefile.exists(): return ''
93+
its = re.split(r'^## ', self.changefile.read_text(), flags=re.MULTILINE)
94+
if not len(its)>0: return ''
95+
return '\n'.join(its[1].splitlines()[1:]).strip()
96+
97+
# %% ../nbs/17_release.ipynb 17
98+
@call_parse
99+
def changelog(
100+
debug:store_true=False, # Print info to be added to CHANGELOG, instead of updating file
101+
repo:str=None, # repo to use instead of `lib_name` from `settings.ini`
102+
):
103+
"Create a CHANGELOG.md file from closed and labeled GitHub issues"
104+
res = Release(repo=repo).changelog(debug=debug)
105+
if debug: print(res)
106+
107+
# %% ../nbs/17_release.ipynb 18
108+
@call_parse
109+
def release_git(
110+
token:str=None # Optional GitHub token (otherwise `token` file is used)
111+
):
112+
"Tag and create a release in GitHub for the current version"
113+
ver = Release(token=token).release()
114+
print(f"Released {ver}")
115+
116+
# %% ../nbs/17_release.ipynb 19
117+
@call_parse
118+
def release_gh(
119+
token:str=None # Optional GitHub token (otherwise `token` file is used)
120+
):
121+
"Calls `Release.changelog`, lets you edit the result, then pushes to git and calls `Release.release_git`"
122+
cfg = _find_config()
123+
Release().changelog()
124+
subprocess.run([os.environ.get('EDITOR','nano'), cfg.config_path/'CHANGELOG.md'])
125+
if not input("Make release now? (y/n) ").lower().startswith('y'): sys.exit(1)
126+
run('git commit -am release')
127+
run('git push')
128+
ver = Release(token=token).release()
129+
print(f"Released {ver}")

0 commit comments

Comments
 (0)