Skip to content

Commit

Permalink
Developer documentation: release guide (#142)
Browse files Browse the repository at this point in the history
* Add script to generate release notes

* Add release procedure guide

* Fix formatting in release_guide.rst

* Update release guide with conda feeddstock info

* Update release_guide.rst
  • Loading branch information
GenevieveBuckley committed Aug 3, 2020
1 parent faedcfb commit cf3bde8
Show file tree
Hide file tree
Showing 2 changed files with 379 additions and 0 deletions.
204 changes: 204 additions & 0 deletions docs/release/generate_release_notes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
"""Generate the release notes automatically from Github pull requests.
Start with:
```
export GH_TOKEN=<your-gh-api-token>
```
Then, for to include everything from a certain release to master:
```
python /path/to/generate_release_notes.py v0.14.0 master --version 0.15.0
```
Or two include only things between two releases:
```
python /path/to/generate_release_notes.py v.14.2 v0.14.3 --version 0.14.3
```
You should probably redirect the output with:
```
python /path/to/generate_release_notes.py [args] | tee release_notes.md
```
You'll require PyGitHub and tqdm, which you can install with:
```
pip install PyGithub>=1.44.1 twine>=3.1.1 tqdm
```
References
https://github.com/scikit-image/scikit-image/blob/master/tools/generate_release_notes.py
https://github.com/scikit-image/scikit-image/issues/3404
https://github.com/scikit-image/scikit-image/issues/3405
"""
import os
import argparse
from datetime import datetime
from collections import OrderedDict
from warnings import warn

from github import Github

try:
from tqdm import tqdm
except ImportError:
warn(
'tqdm not installed. This script takes approximately 5 minutes '
'to run. To view live progressbars, please install tqdm. '
'Otherwise, be patient.'
)

def tqdm(i, **kwargs):
return i


GH = "https://github.com"
GH_USER = 'dask'
GH_REPO = 'dask-image'
GH_TOKEN = os.environ.get('GH_TOKEN')
if GH_TOKEN is None:
raise RuntimeError(
"It is necessary that the environment variable `GH_TOKEN` "
"be set to avoid running into problems with rate limiting. "
"One can be acquired at https://github.com/settings/tokens.\n\n"
"You do not need to select any permission boxes while generating "
"the token."
)

g = Github(GH_TOKEN)
repository = g.get_repo(f'{GH_USER}/{GH_REPO}')


parser = argparse.ArgumentParser(usage=__doc__)
parser.add_argument('from_commit', help='The starting tag.')
parser.add_argument('to_commit', help='The head branch.')
parser.add_argument(
'--version', help="Version you're about to release.", default='0.2.0'
)

args = parser.parse_args()

for tag in repository.get_tags():
if tag.name == args.from_commit:
previous_tag = tag
break
else:
raise RuntimeError(f'Desired tag ({args.from_commit}) not found')

# For some reason, go get the github commit from the commit to get
# the correct date
github_commit = previous_tag.commit.commit
previous_tag_date = datetime.strptime(
github_commit.last_modified, '%a, %d %b %Y %H:%M:%S %Z'
)


all_commits = list(
tqdm(
repository.get_commits(sha=args.to_commit, since=previous_tag_date),
desc=f'Getting all commits between {args.from_commit} '
f'and {args.to_commit}',
)
)
all_hashes = set(c.sha for c in all_commits)


def add_to_users(users, new_user):
if new_user.name is None:
users[new_user.login] = new_user.login
else:
users[new_user.login] = new_user.name


authors = set()
committers = set()
reviewers = set()
users = {}

for commit in tqdm(all_commits, desc="Getting commiters and authors"):
if commit.committer is not None:
add_to_users(users, commit.committer)
committers.add(commit.committer.login)
if commit.author is not None:
add_to_users(users, commit.author)
authors.add(commit.author.login)

# remove these bots.
committers.discard("web-flow")
authors.discard("azure-pipelines-bot")

highlights = OrderedDict()

highlights['Highlights'] = {}
highlights['New Features'] = {}
highlights['Improvements'] = {}
highlights['Bug Fixes'] = {}
highlights['API Changes'] = {}
highlights['Deprecations'] = {}
highlights['Build Tools'] = {}
other_pull_requests = {}

for pull in tqdm(
g.search_issues(
f'repo:{GH_USER}/{GH_REPO} '
f'merged:>{previous_tag_date.isoformat()} '
'sort:created-asc'
),
desc='Pull Requests...',
):
pr = repository.get_pull(pull.number)
if pr.merge_commit_sha in all_hashes:
summary = pull.title
for review in pr.get_reviews():
if review.user is not None:
add_to_users(users, review.user)
reviewers.add(review.user.login)
for key, key_dict in highlights.items():
pr_title_prefix = (key + ': ').lower()
if summary.lower().startswith(pr_title_prefix):
key_dict[pull.number] = {
'summary': summary[len(pr_title_prefix):]
}
break
else:
other_pull_requests[pull.number] = {'summary': summary}


# add Other PRs to the ordered dict to make doc generation easier.
highlights['Other Pull Requests'] = other_pull_requests


# Now generate the release notes
title = (f'{args.version} ({datetime.today().strftime("%Y-%m-%d")})'
'\n------------------')
print(title)

print(
f"""
We're pleased to announce the release of dask-image {args.version}!
"""
)

for section, pull_request_dicts in highlights.items():
print(f'{section}\n')
if len(pull_request_dicts.items()) == 0:
print()
for number, pull_request_info in pull_request_dicts.items():
print(f'* {pull_request_info["summary"]} (#{number})')


contributors = OrderedDict()

contributors['authors'] = authors
contributors['reviewers'] = reviewers
# ignore committers
# contributors['committers'] = committers

for section_name, contributor_set in contributors.items():
print()
if None in contributor_set:
contributor_set.remove(None)
committer_str = (
f'{len(contributor_set)} {section_name} added to this '
'release (alphabetical)'
)
print(committer_str)
print()

for c in sorted(contributor_set, key=lambda x: users[x].lower()):
commit_link = f"{GH}/{GH_USER}/{GH_REPO}/commits?author={c}"
print(f"* `{users[c]} <{commit_link}>`_ - @{c}")
print()
175 changes: 175 additions & 0 deletions docs/release/release_guide.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
=============
Release Guide
=============

This guide documents the ``dask-image`` release process.
It is based on the ``napari`` release guide created by Kira Evans.

This guide is primarily intended for core developers of `dask-image`.
They will need to have a `PyPI <https://pypi.org>`_ account
with upload permissions to the ``dask-image`` package.
They will also need permissions to merge pull requests
in the ``dask-image`` conda-forge feedstock repository:
https://github.com/conda-forge/dask-image-feedstock.

You will also need these additional release dependencies
to complete the release process:


.. code-block:: bash
pip install PyGithub>=1.44.1 twine>=3.1.1 tqdm
Set PyPI password as GitHub secret
----------------------------------

The `dask/dask-image` repository must have a PyPI API token as a GitHub secret.

This likely has been done already, but if it has not, follow
`this guide <https://pypi.org/help/#apitoken>`_ to gain a token and
`this other guide <https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets>`_
to add it as a secret.


Determining the new version number
----------------------------------

We use `semantic versioning <https://medium.com/the-non-traditional-developer/semantic-versioning-for-dummies-45c7fe04a1f8>`_
for `dask-image`. This means version numbers have the format
`Major.Minor.Patch`.

`Versioneer <https://github.com/warner/python-versioneer>`_
then determines the exact version from the latest
`git tag <https://git-scm.com/book/en/v2/Git-Basics-Tagging>`_
beginning with `v`.


Generate the release notes
--------------------------

The release notes contain a list of merges, contributors, and reviewers.

1. Crate a GH_TOKEN environment variable on your computer.

On Linux/Mac:

.. code-block:: bash
export GH_TOKEN=<your-gh-api-token>
On Windows:

.. code-block::
set GH_TOKEN <your-gh-api-token>
If you don't already have a
`personal GitHub API token <https://github.blog/2013-05-16-personal-api-tokens/>`_,
you can create one from the developer settings of your GitHub account:
`<https://github.com/settings/tokens>`_


2. Run the python script to generate the release notes,
including all changes since the last tagged release.

Call the script like this:

.. code-block:: bash
python docs/release/generate_release_notes.py <last-version-tag> master --version <new-version-number>
An example:

.. code-block:: bash
python docs/release/generate_release_notes.py v0.14.0 master --version 0.15.0
See help for this script with:

.. code-block:: bash
python docs/release/generate_release_notes.py -h
3. Scan the PR titles for highlights, deprecations, API changes,
and bugfixes, and mention these in the relevant sections of the notes.
Try to present the information in an expressive way by mentioning
the affected functions, elaborating on the changes and their
consequences. If possible, organize semantically close PRs in groups.

4. Copy your edited release notes into the file ``HISTORY.rst``.

5. Make and merge a PR with the release notes before moving onto the next steps.


Create the release candidate
-----------------------------

Go to the dask-image releases page: https://github.com/dask/dask-image/releases

Click the "Draft Release" button to create a new release candidate.

- Both the tag version and release title should have the format ``vX.Y.Zrc1``.
- Copy-paste the release notes from ``HISTORY.rst`` for this release into the
description text box.

Note here how we are using ``rc`` for release candidate to create a version
of our release we can test before making the real release.

Creating the release will trigger a GitHub actions script,
which automatically uploads the release to PyPI.


Testing the release candidate
-----------------------------

The release candidate can then be tested with

.. code-block:: bash
pip install --pre dask-image
It is recommended that the release candidate is tested in a virtual environment
in order to isolate dependencies.

If the release candidate is not what you want, make your changes and
repeat the process from the beginning but
incrementing the number after ``rc`` (e.g. ``vX.Y.Zrc2``).

Once you are satisfied with the release candidate it is time to generate
the actual release.

Generating the actual release
-----------------------------

To generate the actual release you will now repeat the processes above
but now dropping the ``rc`` suffix from the version number.

This will automatically upload the release to PyPI, and will also
automatically begin the process to release the new version on conda-forge.

Releasing on conda-forge
------------------------

It usually takes about an hour or so for the conda-forge bot
``regro-cf-autotick-bot`` to see that there is a new release
available on PyPI, and open a pull request in the ``dask-image``
conda-forge feedstock here: https://github.com/conda-forge/dask-image-feedstock

Note: the conda-forge bot will not open a PR for any of the release candidates,
only for the final release. Only one PR is opened for

Before merging the pull request, first you should check:
* That all the tests have passed on CI for this pull request
* If any dependencies were changed, and should be updated in the pull request

Once that all looks good you can merge the pull request,
and the newest version of ``dask-image`` will automatically be made
available on conda-forge. We're finished!

0 comments on commit cf3bde8

Please sign in to comment.