{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#hide\n", "# default_exp core" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Create software releases\n", "\n", "> API for auto-generated tagged releases, and release notes (from GitHub issues)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#export\n", "from fastcore.imports import *\n", "from fastcore.utils import *\n", "from fastcore.foundation import *\n", "from fastcore.script import *\n", "from ghapi.core import *\n", "\n", "from datetime import datetime\n", "from configparser import ConfigParser\n", "import shutil,subprocess" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#hide\n", "from nbdev.showdoc import show_doc" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#export\n", "GH_HOST = \"https://api.github.com\"" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#export\n", "def find_config(cfg_name=\"settings.ini\"):\n", " cfg_path = Path().absolute()\n", " while cfg_path != cfg_path.parent and not (cfg_path/cfg_name).exists(): cfg_path = cfg_path.parent\n", " config_file = cfg_path/cfg_name\n", " assert config_file.exists(), f\"Couldn't find {cfg_name}\"\n", " config = ConfigParser()\n", " config.read(config_file)\n", " return config['DEFAULT'],cfg_path" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#export\n", "def _issue_txt(issue):\n", " res = '- {} ([#{}]({}))'.format(issue.title.strip(), issue.number, issue.html_url)\n", " if hasattr(issue, 'pull_request'): res += ', thanks to [@{}]({})'.format(issue.user.login, issue.user.html_url)\n", " res += '\\n'\n", " if not issue.body: return res\n", " return res + f\" - {issue.body.strip()}\\n\"\n", "\n", "def _issues_txt(iss, label):\n", " if not iss: return ''\n", " res = f\"### {label}\\n\\n\"\n", " return res + '\\n'.join(map(_issue_txt, iss))\n", "\n", "def _load_json(cfg, k):\n", " try: return json.loads(cfg[k])\n", " except json.JSONDecodeError as e: raise Exception(f\"Key: `{k}` in .ini file is not a valid JSON string: {e}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## FastRelease -" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#export\n", "class FastRelease:\n", " def __init__(self, owner=None, repo=None, token=None, **groups):\n", " \"Create CHANGELOG.md from GitHub issues\"\n", " self.cfg,cfg_path = find_config()\n", " self.changefile = cfg_path/'CHANGELOG.md'\n", " if not groups:\n", " default_groups=dict(breaking=\"Breaking Changes\", enhancement=\"New Features\", bug=\"Bugs Squashed\")\n", " groups=_load_json(self.cfg, 'label_groups') if 'label_groups' in self.cfg else default_groups\n", " os.chdir(cfg_path)\n", " owner,repo = owner or self.cfg['user'], repo or self.cfg['lib_name']\n", " token = ifnone(token, os.getenv('FASTRELEASE_TOKEN',None))\n", " if not token and Path('token').exists(): token = Path('token').read_text().strip()\n", " if not token: raise Exception('Failed to find token')\n", " self.gh = GhApi(owner, repo, token)\n", " self.groups = groups\n", "\n", " def _issues(self, label):\n", " return self.gh.issues.list_for_repo(state='closed', sort='created', filter='all', since=self.commit_date, labels=label)\n", " def _issue_groups(self): return parallel(self._issues, self.groups.keys(), progress=False)\n", "\n", " def changelog(self, debug=False):\n", " \"Create the CHANGELOG.md file, or return the proposed text if `debug` is `True`\"\n", " if not self.changefile.exists(): self.changefile.write_text(\"# Release notes\\n\\n\\n\")\n", " marker = '\\n'\n", " try: self.commit_date = self.gh.repos.get_latest_release().published_at\n", " except HTTP404NotFoundError: self.commit_date = '2000-01-01T00:00:004Z'\n", " res = f\"\\n## {self.cfg['version']}\\n\"\n", " issues = self._issue_groups()\n", " res += '\\n'.join(_issues_txt(*o) for o in zip(issues, self.groups.values()))\n", " if debug: return res\n", " res = self.changefile.read_text().replace(marker, marker+res+\"\\n\")\n", " shutil.copy(self.changefile, self.changefile.with_suffix(\".bak\"))\n", " self.changefile.write_text(res)\n", " run(f'git add {self.changefile}')\n", "\n", " def release(self):\n", " \"Tag and create a release in GitHub for the current version\"\n", " ver = self.cfg['version']\n", " notes = self.latest_notes()\n", " self.gh.create_release(ver, body=notes)\n", " return ver\n", "\n", " def latest_notes(self):\n", " \"Latest CHANGELOG entry\"\n", " if not self.changefile.exists(): return ''\n", " its = re.split(r'^## ', self.changefile.read_text(), flags=re.MULTILINE)\n", " if not len(its)>0: return ''\n", " return '\\n'.join(its[1].splitlines()[1:]).strip()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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:\n", "\n", "- **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`.\n", "- **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`.\n", "- **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.\n", "- **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:\n", "\n", "```python\n", "{\"breaking\": \"Breaking Changes\", \"enhancement\":\"New Features\", \"bug\":\"Bugs Squashed\"}\n", "```" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "rel = FastRelease()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "data": { "text/markdown": [ "
FastRelease.changelog
[source]FastRelease.changelog
(**`debug`**=*`False`*)\n",
"\n",
"Create the CHANGELOG.md file, or return the proposed text if `debug` is `True`"
],
"text/plain": [
"FastRelease.release
[source]FastRelease.release
()\n",
"\n",
"Tag and create a release in GitHub for the current version"
],
"text/plain": [
"