-
-
Notifications
You must be signed in to change notification settings - Fork 46
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Developer documentation: release guide (#142)
* 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
1 parent
faedcfb
commit cf3bde8
Showing
2 changed files
with
379 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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! | ||
|