{ "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]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": [
"FastRelease.latest_release
[source]FastRelease.latest_release
()\n",
"\n",
"Tag for the latest release"
],
"text/plain": [
"FastRelease.gh
[source]FastRelease.gh
(**`path`**, **`complete`**=*`False`*)\n",
"\n",
"Call GitHub API `path`"
],
"text/plain": [
"