{ "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "#hide\n", "# default_exp core" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Create software releases\n", "\n", "> Auto-generated tagged releases, and release notes (from GitHub issues)" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "#export\n", "from datetime import datetime\n", "from textwrap import fill\n", "from urllib.request import Request,urlopen\n", "from urllib.error import HTTPError\n", "from urllib.parse import urlencode\n", "from concurrent.futures import ProcessPoolExecutor\n", "from pathlib import Path\n", "from configparser import ConfigParser\n", "import os,shutil,json,subprocess" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "#hide\n", "from nbdev.showdoc import show_doc" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "#export\n", "GH_HOST = \"https://api.github.com\"" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "#export\n", "def _issue_txt(issue):\n", " res = '- {} ([#{}]({}))\\n'.format(issue[\"title\"].strip(), issue[\"number\"], issue[\"url\"])\n", " body = issue['body']\n", " if not body: return res\n", " return res + fill(body.strip(), initial_indent=\" - \", subsequent_indent=\" \") + \"\\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 _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": "markdown", "metadata": {}, "source": [ "## Helper functions" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "#export\n", "def run_proc(*args):\n", " res = subprocess.run(args, capture_output=True)\n", " if res.returncode: raise IOError(subprocess.stdout + \";;\" + subprocess.stderr)\n", " return res.stdout" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "#export\n", "def do_request(url, post=False, headers=None, **data):\n", " \"Call GET or json-encoded POST on `url`, depending on `post`\"\n", " if data:\n", " if post: data = json.dumps(data).encode('ascii')\n", " else:\n", " url += \"?\" + urlencode(data)\n", " data = None\n", " with urlopen(Request(url, headers=headers, data=data or None)) as res: return json.loads(res.read())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## FastRelease -" ] }, { "cell_type": "code", "execution_count": 8, "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", " if not groups: groups = dict(breaking=\"Breaking Changes\", enhancement=\"New Features\", bug=\"Bugs Squashed\")\n", " self.cfg,cfg_path = _config()\n", " os.chdir(cfg_path)\n", " if not owner: owner = self.cfg['user']\n", " if not repo: repo = self.cfg['lib_name']\n", " if not token:\n", " assert Path('token').exists, \"Failed to find token\"\n", " token = Path('token').read_text().strip()\n", " self.owner,self.repo,self.token,self.groups = owner,repo,token,groups\n", " self.headers = { 'Authorization' : 'token ' + token }\n", " self.repo_url = f\"{GH_HOST}/repos/{owner}/{repo}\"\n", "\n", " def gh(self, path, complete=False, post=False, **data):\n", " \"Call GitHub API `path`\"\n", " if not complete: path = f\"{self.repo_url}/{path}\"\n", " return do_request(path, headers=self.headers, post=post, **data)\n", "\n", " def _tag_date(self, tag):\n", " try: tag_d = self.gh(f\"git/ref/tags/{tag}\")\n", " except HTTPError: raise Exception(f\"Failed to find tag {tag}\")\n", " commit_d = self.gh(tag_d[\"object\"][\"url\"], complete=True)\n", " self.commit_date = commit_d[\"committer\"][\"date\"]\n", " return self.commit_date\n", "\n", " def _issues(self, label):\n", " return self.gh(\"issues\", state='closed', sort='created', filter='all',\n", " since=self.commit_date, labels=label)\n", " \n", " def _issue_groups(self):\n", " with ProcessPoolExecutor() as ex: return ex.map(self._issues, self.groups.keys())\n", " \n", " def latest_release(self):\n", " \"Tag for the latest release\"\n", " return self.gh(\"releases/latest\")[\"tag_name\"]\n", "\n", " def changelog(self, debug=False):\n", " \"Create the CHANGELOG.md file, or return the proposed text if `debug` is `True`\"\n", " fn = Path('CHANGELOG.md')\n", " if not fn.exists(): fn.write_text(\"# Release notes\\n\\n\\n\")\n", " txt = fn.read_text()\n", " marker = '\\n'\n", " try:\n", " latest = self.latest_release()\n", " self._tag_date(latest)\n", " except HTTPError: # no prior releases\n", " 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", " res = txt.replace(marker, marker+res+\"\\n\")\n", " if debug: return res\n", " shutil.copy(fn, fn+\".bak\")\n", " Path(fn).write_text(res)\n", "\n", " def release(self):\n", " \"Tag and create a release in GitHub for the current version\"\n", " ver = self.cfg['version']\n", " run_proc('git', 'tag', ver)\n", " run_proc('git', 'push', '--tags')\n", " run_proc('git', 'pull', '--tags')\n", " self.gh(\"releases\", post=True, tag_name=ver, name=ver, body=ver)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To create a markdown changelog, first create a `FastRelease` object, passing a mapping from GitHub labels to markdown titles. Put your github token in a file named `token` at the root of your repo." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/markdown": [ "

FastRelease.changelog[source]

\n", "\n", "> FastRelease.changelog(**`debug`**=*`False`*)\n", "\n", "Create the CHANGELOG.md file, or return the proposed text if `debug` is `True`" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "show_doc(FastRelease.changelog)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The default mapping from labels to markdown headings is:\n", "\n", "- **breaking**: \"Breaking Changes\"\n", "- **enhancement**: \"New Features\"\n", "- **bug**: \"Bugs Squashed\"" ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "scrolled": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "/home/jhoward/git/fastrelease\n", "# Release notes\n", "\n", "\n", "\n", "## 0.0.1\n", "\n", "\n", "\n", "\n" ] } ], "source": [ "rel = FastRelease()\n", "print(rel.changelog(debug=True))" ] }, { "cell_type": "code", "execution_count": 204, "metadata": {}, "outputs": [ { "data": { "text/markdown": [ "

FastRelease.release[source]

\n", "\n", "> FastRelease.release()\n", "\n", "Tag and create a release in GitHub for the current version" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "show_doc(FastRelease.release)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This uses the version information from your `settings.ini`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Utility methods" ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [ { "data": { "text/markdown": [ "

FastRelease.latest_release[source]

\n", "\n", "> FastRelease.latest_release()\n", "\n", "Tag for the latest release" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "show_doc(FastRelease.latest_release)" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "data": { "text/markdown": [ "

FastRelease.gh[source]

\n", "\n", "> FastRelease.gh(**`path`**, **`complete`**=*`False`*)\n", "\n", "Call GitHub API `path`" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "show_doc(FastRelease.gh)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Export-" ] }, { "cell_type": "code", "execution_count": 107, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Converted 00_export.ipynb.\n", "Converted 01_sync.ipynb.\n", "Converted 02_showdoc.ipynb.\n", "Converted 03_export2html.ipynb.\n", "Converted 04_test.ipynb.\n", "Converted 05_merge.ipynb.\n", "Converted 05a_conda.ipynb.\n", "Converted 06_cli.ipynb.\n", "Converted 07_clean.ipynb.\n", "Converted 10_release.ipynb.\n", "Converted 99_search.ipynb.\n", "Converted index.ipynb.\n", "Converted magic_flags.ipynb.\n", "Converted nbdev_callbacks.ipynb.\n", "Converted tutorial.ipynb.\n" ] } ], "source": [ "#hide\n", "from nbdev.export import notebook2script\n", "notebook2script()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.7" }, "toc": { "base_numbering": 1, "nav_menu": {}, "number_sections": false, "sideBar": true, "skip_h1_title": true, "title_cell": "Table of Contents", "title_sidebar": "Contents", "toc_cell": false, "toc_position": {}, "toc_section_display": true, "toc_window_display": false } }, "nbformat": 4, "nbformat_minor": 4 }