Auto-generated tagged releases and release notes from GitHub issues
Overview
nbdev.release provides 3 commands that you can run from your shell to manage your changelog file and git releases:
nbdev-changelog: creates a CHANGELOG.md file from closed and labeled GitHub issues
nbdev-release-git: tags and creates a release in GitHub for the current version
nbdev-release-gh: calls nbdev-changelog, lets you edit the result, then pushes to git and calls nbdev-release-git
It provides 3 futher commands for releasing packages on pypi or conda:
nbdev-pypi: Create and upload a pypi installer
nbdev-conda: Create and upload a conda installer
nbdev-release-both: Create and upload both pypi and conda installers
Here’s a brief demonstration of how to use the changelog and git release tools in nbdev.release. This demo first creates an issue using the gh command line tool, and then closes it using git; you can also use GitHub’s web interface for both of these tasks. (Note that this functionality used to be in a project called fastrelease, so in the video the command line tools have different names, starting with fastrelease_ instead of nbdev-).
Setup
You’ll need to get a GitHub personal access token if you haven’t already. To do so, click here and enter “nbdev” in the “Note” section, and click the repo checkbox.
Then click “Generate Token” at the bottom of the screen, and copy the token (the long string of letters and numbers shown). You can easily do that by clicking the little clipboard icon next to the token.
It’s easiest to save the token as an environment variable GITHUB_TOKEN that can be automatically accessed. We recommend you do this is by adding the following to the end of your .bashrc or .zshrc file:
exportGITHUB_TOKEN=xxx
…and then replace the xxx with the token you just copied. It will automatically be available whenever you start a new shell (but don’t forget to source that file or open a new shell after you change it.).
Creating release notes
Now you’re ready to create your release notes. These are created in a file called CHANGELOG.md. Here’s an example of what it creates: nbdev CHANGELOG.
All issues with the label bug, enhancement, or breaking that have been closed in your repo since your last release will be added to the top of this file. If you haven’t made any releases before, then all issues with those labels will be included.
Therefore, before you create or update CHANGELOG.md, go to your GitHub issues page, remove is:open from the filter, and label any issues you want included with one of the labels above. When you’ve done that, you can create or update your release notes by running in your terminal:
nbdev-changelog
Each issue title will be added. Open CHANGELOG.md in your editor and make any edits that you want, and then commit the file to your repo (remember to git add it!)
Tagging a release
You should now tag a release. This will create a tag in GitHub with your current version number in __init__.py, and will then make it into a release, using your latest release notes as the description of the release:
nbdev-release-git
After you run this, be sure to increment your version number. You can either edit it manually, or if you use nbdev it can be done for you by running:
nbdev-bump-version
Doing both (creating release notes, and tagging a release)
To complete both of the steps above, run:
nbdev-release-gh
If the changelog was generated and reviewed separately, complete the release without regenerating it, opening an editor, or prompting:
nbdev-release-gh --no_changelog --no_editor --yes
Automatic changelog generation stops when the current branch does not contain the latest GitHub release, as happens for an update to an older version. Write CHANGELOG.md manually and use the command above. The next section shows the complete process.
See the screencast above for a demonstration of the interactive workflow.
Releasing an update to an older version
Sometimes the latest release is 2.x, but you need to fix version 1.x. The example below starts from release 1.1.0 and publishes 1.1.1.
A Git worktree is a second project folder connected to the same repository. It lets you open the older version without changing your usual project folder:
Write the new CHANGELOG.md entry yourself. Automatic changelog generation is intended for the latest release, so skip it here. Review the changes before releasing:
git diffgit status --shortnbdev-release-gh--no_changelog--no_editor--yesnbdev-pypinbdev-bump-versiongit commit -am bumpgit push
The release command pushes the 1.x branch to GitHub, then tags the code it released. Because 1.x is not the repository’s default branch, the release is not marked as “Latest” on GitHub, so the repo front page keeps showing the newest version. After checking the GitHub release and PyPI package, remove the extra folder:
cd ../myprojectgit worktree remove ../myproject-1.x
The extra folder uses a little more disk space, and you need to run each command in the right folder. In return, the latest and older versions remain available at the same time. This is useful when other terminals or Python sessions are using the main project folder.
For a small update with a clean project folder, you can instead switch that folder to the older release:
git fetch origin --tagsgit switch -c 1.x 1.1.0
Then follow the same bump, fix, changelog, test, and release steps above. When finished, return to the latest code:
git switch maingit pull --ff-only
This alternative uses one folder and fewer commands. While the release is in progress, however, your usual project folder and any editable Python installation based on it will contain the older version.
Python API
Releases target the exact pushed commit. Automatic changelogs are limited to the latest release line; maintenance releases use a manually written changelog.
def update_changelog( txt, ver, notes, marker:str='<!-- do not remove -->\n'):
Insert notes into changelog txt after marker, replacing any existing section for ver
update_changelog makes changelog generation idempotent: if the changelog already has a section for ver (e.g. from an earlier run whose release never happened), it’s replaced rather than duplicated. That also means regenerating discards any manual edits to that pending section, so curate the changelog after its final generation. It also owns the spacing: two blank lines between version sections and a single newline at EOF, whether or not notes has trailing newlines.
log ="# Release notes\n\n<!-- do not remove -->\n\n## 0.0.1\n\n- First release\n"test_eq(update_changelog("# Release notes\n\n<!-- do not remove -->\n", '0.0.1', '\n## 0.0.1\n\n- First release'), log)upd = update_changelog(log, '0.0.2', '\n## 0.0.2\n\n- New feature')assert upd.index('## 0.0.2') < upd.index('## 0.0.1')assert'- New feature\n\n\n## 0.0.1'in updprint(upd)
# Release notes
<!-- do not remove -->
## 0.0.2
- New feature
## 0.0.1
- First release
Regenerating notes for a version that already has a pending section gives the same result as generating them fresh:
To create a markdown changelog, first create a Release 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. Release attempts to fetch values for arguments from the following locations if not supplied:
owner: fetched from the GitHub URL in [project.urls] in pyproject.toml. 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 name in [project] in pyproject.toml. This is the name of the repository on GitHub. For example for the repo fastai/fastcore the repo would be fastcore. You can instead pass "owner/repo" (e.g. "fastai/fastcore") to set both at once.
token: fetched from a file named token at the root of your repo. Creating a token is discussed in the setup section.
groups: (optional) fetched from the field label_groups in [tool.nbdev] in pyproject.toml. This is a mapping from label names to titles in your release notes. If not specified, this defaults to:
Tag and create a release in GitHub for the current version
This uses the version information from your __init__.py. When releasing from a branch other than the repo’s default branch (e.g. a fix to an older version), the release is not marked as the repo’s latest on GitHub.
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.
asyncdef changelog( debug:store_true=False, # Print info to be added to CHANGELOG, instead of updating file repo:str=None, # "repo" or "owner/repo" to use instead of pyproject.toml values token:str=None, # Optional GitHub token (otherwise `token` file is used)):
Create a CHANGELOG.md file from closed and labeled GitHub issues
asyncdef release_gh( token:str=None, # Optional GitHub token (otherwise `token` file is used) repo:str=None, # "repo" or "owner/repo" to use instead of pyproject.toml values no_changelog:store_true=False, # Skip changelog creation (assumes CHANGELOG.md is up to date) no_editor:store_true=False, # Skip opening CHANGELOG.md in an editor yes:store_true=False, # Release without asking for confirmation):
Create the changelog, optionally edit it, then push and create the GitHub release
def release_conda( path:str='conda', # Path where package will be created do_build:bool_arg=True, # Run `conda build` step build_args:str='', # Additional args (as str) to send to `conda build` skip_upload:store_true=False, # Skip `anaconda upload` step mambabuild:store_true=False, # Use `mambabuild` (requires `boa`) upload_user:str=None, # Optional user to upload package to):
Create a meta.yaml file ready to be built into a package, and optionally build and upload it
Add --debug to the conda build command to debug any problems that occur. Note that the build step takes a few minutes. Add -u {org_name} to the anaconda upload command if you wish to upload to an organization, or pass upload_user to nbdev-conda-package.
NB: you need to first of all upload your package to PyPi, before creating the conda package.
def release_both( path:str='conda', # Path where package will be created do_build:bool_arg=True, # Run `conda build` step build_args:str='', # Additional args (as str) to send to `conda build` skip_upload:store_true=False, # Skip `anaconda upload` step mambabuild:store_true=False, # Use `mambabuild` (requires `boa`) upload_user:str=None, # Optional user to upload package to repository:str='pypi', # Pypi respository to upload to (defined in ~/.pypirc)):