<jupyter_start><jupyter_text>Create software releases> API for auto-generated tagged releases, and release notes (from GitHub issues)<jupyter_code>#export
from fastcore.imports import *
from fastcore.utils import *
from fastcore.foundation import *
from fastcore.script import *
from ghapi.core import *

from datetime import datetime
from configparser import ConfigParser
import shutil,subprocess
#hide
from nbdev.showdoc import show_doc
#export
GH_HOST = "https://api.github.com"
#export
def find_config(cfg_name="settings.ini"):
    cfg_path = Path().absolute()
    while cfg_path != cfg_path.parent and not (cfg_path/cfg_name).exists(): cfg_path = cfg_path.parent
    return Config(cfg_path, cfg_name)
#export
def _issue_txt(issue):
    res = '- {} ([#{}]({}))'.format(issue.title.strip(), issue.number, issue.html_url)
    if hasattr(issue, 'pull_request'): res += ', thanks to [@{}]({})'.format(issue.user.login, issue.user.html_url)
    res += '\n'
    if not issue.body: return res
    return res + f"  - {issue.body.strip()}\n"

def _issues_txt(iss, label):
    if not iss: return ''
    res = f"### {label}\n\n"
    return res + '\n'.join(map(_issue_txt, iss))

def _load_json(cfg, k):
    try: return json.loads(cfg[k])
    except json.JSONDecodeError as e: raise Exception(f"Key: `{k}` in .ini file is not a valid JSON string: {e}")<jupyter_output><empty_output><jupyter_text>FastRelease -<jupyter_code>#export
class FastRelease:
    def __init__(self, owner=None, repo=None, token=None, **groups):
        "Create CHANGELOG.md from GitHub issues"
        self.cfg = find_config()
        self.changefile = self.cfg.config_path/'CHANGELOG.md'
        if not groups:
            default_groups=dict(breaking="Breaking Changes", enhancement="New Features", bug="Bugs Squashed")
            groups=_load_json(self.cfg, 'label_groups') if 'label_groups' in self.cfg else default_groups
        os.chdir(self.cfg.config_path)
        owner,repo = owner or self.cfg.user, repo or self.cfg.lib_name
        token = ifnone(token, os.getenv('FASTRELEASE_TOKEN',None))
        if not token and Path('token').exists(): token = Path('token').read_text().strip()
        if not token: raise Exception('Failed to find token')
        self.gh = GhApi(owner, repo, token)
        self.groups = groups

    def _issues(self, label):
        return self.gh.issues.list_for_repo(state='closed', sort='created', filter='all', since=self.commit_date, labels=label)
    def _issue_groups(self): return parallel(self._issues, self.groups.keys(), progress=False)

    def changelog(self, debug=False):
        "Create the CHANGELOG.md file, or return the proposed text if `debug` is `True`"
        if not self.changefile.exists(): self.changefile.write_text("# Release notes\n\n<!-- do not remove -->\n")
        marker = '<!-- do not remove -->\n'
        try: self.commit_date = self.gh.repos.get_latest_release().published_at
        except HTTP404NotFoundError: self.commit_date = '2000-01-01T00:00:004Z'
        res = f"\n## {self.cfg.version}\n"
        issues = self._issue_groups()
        res += '\n'.join(_issues_txt(*o) for o in zip(issues, self.groups.values()))
        if debug: return res
        res = self.changefile.read_text().replace(marker, marker+res+"\n")
        shutil.copy(self.changefile, self.changefile.with_suffix(".bak"))
        self.changefile.write_text(res)
        run(f'git add {self.changefile}')

    def release(self):
        "Tag and create a release in GitHub for the current version"
        ver = self.cfg.version
        notes = self.latest_notes()
        self.gh.create_release(ver, body=notes)
        return ver

    def latest_notes(self):
        "Latest CHANGELOG entry"
        if not self.changefile.exists(): return ''
        its = re.split(r'^## ', self.changefile.read_text(), flags=re.MULTILINE)
        if not len(its)>0: return ''
        return '\n'.join(its[1].splitlines()[1:]).strip()<jupyter_output><empty_output><jupyter_text>To create a markdown changelog, first create a `FastRelease` object, optionally passing a mapping from GitHub labels to markdown titles. Put your github token in a file named `token` at the root of your repo.  `FastRelease` attempts to fetch values for arguments from the following locations if not supplied:- **owner:** fetched from the field `user` in `settings.ini`.  This is the owner name of the repository on GitHub. For example for the repo `fastai/fastcore` the owner would be `fastai`.- **repo:** fetched from the field `lib_name` in `settings.ini`.  This is the name of the repository on GitHub.  For example for the repo `fastai/fastcore` the owner would be `fastcore`.- **token:** fetched from a file named `token` at the root of your repo.  Creating a token is discussed in [the setup](https://fastrelease.fast.ai/Set-up) section.- **groups:** (optional) fetched from the field `label_groups` in `settings.ini`, which is a JSON string.  This is a mapping from label names to titles in your release notes. If not specified, this defaults to:```python{"breaking": "Breaking Changes", "enhancement":"New Features", "bug":"Bugs Squashed"}```<jupyter_code>#slow
rel = FastRelease()
show_doc(FastRelease.changelog)<jupyter_output><empty_output><jupyter_text>All relevant pull requests and issues are fetched from the GitHub API, and are categorized according to a user-supplied mapping from labels to markdown headings.<jupyter_code># print(rel.changelog(debug=True))
show_doc(FastRelease.release)<jupyter_output><empty_output><jupyter_text>This uses the version information from your `settings.ini`.  CLI functions<jupyter_code>#export
@call_parse
def fastrelease_changelog(
    debug:store_true=False  # Print info to be added to CHANGELOG, instead of updating file
):
    "Create a CHANGELOG.md file from closed and labeled GitHub issues"
    FastRelease().changelog(debug=debug)
#export
@call_parse
def fastrelease_release(
    token:str=None  # Optional GitHub token (otherwise `token` file is used)
):
    "Tag and create a release in GitHub for the current version"
    ver = FastRelease(token=token).release()
    print(f"Released {ver}")
#export
@call_parse
def fastrelease(
    debug:store_true=False,  # Print info to be added to CHANGELOG, instead of updating file
    token:str=None  # Optional GitHub token (otherwise `token` file is used)
):
    "Calls `fastrelease_changelog`, lets you edit the result, then pushes to git and calls `fastrelease_release`"
    cfg = find_config()
    FastRelease().changelog()
    if debug: return
    subprocess.run([os.environ.get('EDITOR','nano'), cfg.config_path/'CHANGELOG.md'])
    if not input("Make release now? (y/n) ").lower().startswith('y'): sys.exit(1)
    run('git commit -am release')
    run('git push')
    ver = FastRelease(token=token).release()
    print(f"Released {ver}")
#export
def bump_version(version, part=2):
    version = version.split('.')
    version[part] = str(int(version[part]) + 1)
    for i in range(part+1, 3): version[i] = '0'
    return '.'.join(version)
test_eq(bump_version('0.1.1'   ), '0.1.2')
test_eq(bump_version('0.1.1', 1), '0.2.0')
#export
@call_parse
def fastrelease_bump_version(
    part:int=2  # Part of version to bump
):
    "Increment version in `settings.py` by one"
    cfg = find_config()
    print(f'Old version: {cfg.version}')
    cfg['version'] = bump_version(cfg.version, part)
    cfg.save()
    print(f'New version: {cfg.version}')<jupyter_output><empty_output><jupyter_text>Export-<jupyter_code>#hide
from nbdev.export import notebook2script
notebook2script()<jupyter_output>Converted 00_core.ipynb.
Converted 01_conda.ipynb.
Converted index.ipynb.