Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
4837c74
Add a script to automate some of the release process.
wsanchez Apr 2, 2020
5234723
Update to 20.4.0rc1
wsanchez Apr 2, 2020
0d0d0a6
Ignore E211 here because black has a different opinion WRT exec.
wsanchez Apr 2, 2020
0415f73
Don't puke on lack of type hints from setuptools
wsanchez Apr 2, 2020
8767e86
Incremental doesn't produce black-compatible output.
wsanchez Apr 2, 2020
735c6ef
Remove coverage-py27-twtrunk, which will no longer succeed.
wsanchez Apr 2, 2020
a7ddee5
Add some docstrings.
wsanchez Apr 2, 2020
fd5e16b
Get rid of coverage-py27-twtrunk harder
wsanchez Apr 2, 2020
4348b96
Check branch prior to bumping version
wsanchez Apr 2, 2020
f52a443
Run black-reformat after incrementing version
wsanchez Apr 2, 2020
c70570f
Missing return type
wsanchez Apr 6, 2020
13a8833
Merge branch 'master' into release-20.4
wsanchez Apr 7, 2020
020841e
Merge branch 'master' into release-20.4
wsanchez Apr 20, 2020
35da29c
Merge branch 'master' into release-20.4
wsanchez Apr 27, 2020
2f2cbfc
Merge branch 'master' into release-20.4
wsanchez May 19, 2020
45e68d7
use click
wsanchez May 22, 2020
ca9640d
publishRelease() now tags the repo and pushes the tag
wsanchez May 22, 2020
91d15ed
Don't need whitelist_externals, pass SSH_AUTH_SOCK through.
wsanchez May 22, 2020
c20ba85
Add publish
wsanchez May 26, 2020
c8b9705
Clean up docs.
wsanchez May 26, 2020
d14ca5d
Doc tweak
wsanchez May 26, 2020
c273126
Let's go ahead and call this 20.6 instead
wsanchez May 26, 2020
2cd8081
Simplify diff
wsanchez May 26, 2020
b4ec473
Merge branch 'master' into release-20.4
wsanchez May 26, 2020
dd51385
Merge branch 'master' into release-20.4
wsanchez May 27, 2020
e84729e
Annotate support for Python 3.8
wsanchez May 27, 2020
9b54953
Merge branch 'master' into release-20.4
wsanchez Jun 2, 2020
b60cbe2
Capture some news for this release.
wsanchez Jun 3, 2020
c565558
Merge branch 'master' into release-20.4
wsanchez Jun 7, 2020
e50767c
Add copyright
wsanchez Jun 7, 2020
e256bed
Fix docstring
wsanchez Jun 7, 2020
955c016
Not imminent dropping for Python 3.5.
wsanchez Jun 7, 2020
6a04b24
Fix description for release environment
wsanchez Jun 7, 2020
11edde4
Note forms & sessions added.
wsanchez Jun 8, 2020
16b4ff8
Set release date
wsanchez Jun 8, 2020
a1f597a
Update version to [klein, version 20.6.0]
wsanchez Jun 8, 2020
04a7048
Revert "Update version to [klein, version 20.6.0]"
wsanchez Jun 8, 2020
6dbe083
Commit after updating final version
wsanchez Jun 8, 2020
699d7b8
Update version to [klein, version 20.6.0]
wsanchez Jun 8, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions NEWS.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,39 @@
NEWS
====

20.6.0 - 2020-06-07
-------------------
* This is the last release of Klein expected to support Python 2.
* This is the last release of Klein expected to support Python 3.5.
* Python 3.4 is no longer supported by Klein. [`#284 <https://github.com/twisted/klein/pull/284>`_]
* Python 3.8 is now supported by Klein. [`#303 <https://github.com/twisted/klein/pull/303>`_]
* ``klein.app.subroute`` is now also available as ``klein.subroute``. [`#293 <https://github.com/twisted/klein/pull/293>`_]
* Support for forms and sessions. [`#276 <https://github.com/twisted/klein/pull/276>`_]
* The ``Klein`` class now supports deep copy by implementing ``__copy__``. [`#74 <https://github.com/twisted/klein/pull/74>`_]

19.6.0 - 2019-06-07
-------------------

New "forms" and "sessions" subsystems provide official support for POST requests, including CSRF protection, form generation to include CSRF tokens, dependency injection to populate parameters from both the request and session, as well as lightweight JSON API support.

17.10.0 - 2017-10-22
--------------------

17.2.0 - 2017-03-03
-------------------

16.12.0 - 2016-12-13
--------------------

15.3.1 - 2015-12-17
-------------------

15.2.0 - 2015-11-30
-------------------

15.1.0 - 2015-07-08
-------------------

15.0.0 - 2015-01-11
-------------------
* [BUG] Klein now includes its test package as part of the distribution. [`#65 <https://github.com/twisted/klein/pull/65>`_]
Expand Down
23 changes: 10 additions & 13 deletions docs/release.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,15 @@ Each version is numbered with the major portion being the last two digits of the
That is, the first release of 2016 would be 16.0, and the second would be 16.1.


Doing a Release
Releasing Klein
---------------

#. Create a branch called "release-<version>"
#. Run :code:`python incremental.update Klein --rc && python incremental.update Klein`
#. Commit, and push the branch
#. Open a PR from the branch (follow the usual process for merging a PR).
#. Pull latest :code:`master`: :code:`git checkout master && git pull --rebase`
#. Clear the directory of any other changes using ``git clean -f -x -d .``
#. Tag the release using ``git tag -s <release> -m "Tag <release> release"``
#. Push up the tag using ``git push --tags``.
#. Make a pull request for this changes.
Continue when it is merged.
#. Generate the tarball and wheel using ``python setup.py sdist bdist_wheel``.
#. Upload the tarball and wheel using ``twine upload dist/klein-*``.
#. Start with a clean (no changes) source tree on the master branch.
#. Create a new release candidate: :code:`tox -e release -- start`
#. Commit and push the branch
#. Open a PR from the branch (follow the usual process for opening a PR).
#. As appropriate, pull the latest code from :code:`master`: :code:`git checkout master && git pull --rebase` (or use the GitHub UI)
#. To publish a release candidate to PyPI: :code:`tox -e release -- publish`
#. Obtain an approving review for the PR using the usual process.
#. To publish a production release: :code:`tox -e release -- publish --final`
#. Merge the PR to the master branch.
317 changes: 317 additions & 0 deletions release.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.

from enum import Enum
from os import chdir
from pathlib import Path
from shutil import rmtree
from subprocess import CalledProcessError, run
from sys import exit, stderr
from tempfile import mkdtemp
from typing import Any, Dict, NoReturn, Optional, Sequence

from click import group as commandGroup, option as commandOption

from git import Repo as Repository, TagReference
from git.refs.head import Head

from incremental import Version


class PyPI(Enum):
Test = "testpypi"
Production = "pypi"


def warning(message: str) -> None:
"""
Print a warning.
"""
print(f"WARNING: {message}", file=stderr)


def error(message: str, exitStatus: int) -> NoReturn:
"""
Print an error message and exit with the given status.
"""
print(f"ERROR: {message}", file=stderr)
exit(exitStatus)


def spawn(args: Sequence[str]) -> None:
"""
Spawn a new process with the given arguments, raising L{SystemExit} with
captured output if the exit status is non-zero.
"""
print("Executing command:", " ".join(repr(arg) for arg in args))
try:
run(args, input=b"", capture_output=True, check=True)
except CalledProcessError as e:
error(f"command {e.cmd} failed: {e.stderr}", 1)


def currentVersion() -> Version:
"""
Determine the current version.
"""
# Incremental doesn't have an API to do this, so we are duplicating some
# code from its source tree. Boo.
versionInfo: Dict[str, Any] = {}
versonFile = Path(__file__).parent / "src" / "klein" / "_version.py"
exec (versonFile.read_text(), versionInfo) # noqa: E211 # black py2.7
return versionInfo["__version__"]


def fadeToBlack() -> None:
"""
Run black to reformat the source code.
"""
spawn(["tox", "-e", "black-reformat"])


def incrementVersion(candidate: bool) -> None:
"""
Increment the current release version.
If C{candidate} is C{True}, the new version will be a release candidate;
otherwise it will be a regular release.
"""
# Incremental doesn't have an API to do this, so we have to run a
# subprocess. Boo.
args = ["python", "-m", "incremental.update", "klein"]
if candidate:
args.append("--rc")
spawn(args)

# Incremental generates code that black wants to reformat.
fadeToBlack()


def releaseBranchName(version: Version) -> str:
"""
Compute the name of the release branch for the given version.
"""
return f"release-{version.major}.{version.minor}"


def releaseBranch(repository: Repository, version: Version) -> Optional[Head]:
"""
Return the release branch corresponding to the given version.
"""
branchName = releaseBranchName(version)

if branchName in repository.heads:
return repository.heads[branchName]

return None


def releaseTagName(version: Version) -> str:
"""
Compute the name of the release tag for the given version.
"""
return version.public()


def createReleaseBranch(repository: Repository, version: Version) -> Head:
"""
Create a new release branch.
"""
branchName = releaseBranchName(version)

if branchName in repository.heads:
error(f'Release branch "{branchName}" already exists.', 1)

print(f'Creating release branch: "{branchName}"')
return repository.create_head(branchName)


def clone(repository: Repository, tag: TagReference) -> Path:
"""
Clone a tagged version from the given repository's origin.
Return the path to the new clone.
"""
path = Path(mkdtemp())

print(f"Cloning repository with tag {tag} at {path}...")
Repository.clone_from(
url=next(repository.remotes.origin.urls),
to_path=str(path),
branch=tag.name,
multi_options=["--depth=1"],
)

return path


def distribute(
repository: Repository, tag: TagReference, test: bool = False
) -> None:
"""
Build a distribution for the project at the given path and upload to PyPI.
"""
src = clone(repository, tag)

if test:
pypi = PyPI.Test
else:
pypi = PyPI.Production

wd = Path.cwd()
try:
chdir(src)

print("Building distribution at:", src)
spawn(["python", "setup.py", "sdist", "bdist_wheel"])

print(f"Uploading distribution to {pypi.value}...")
twineCommand = ["twine", "upload"]
twineCommand.append(f"--repository={pypi.value}")
twineCommand += [str(p) for p in Path("dist").iterdir()]
spawn(twineCommand)

finally:
chdir(wd)

rmtree(str(src))


def startRelease() -> None:
"""
Start a new release:
* Increment the current version to a new release candidate version.
* Create a corresponding branch.
* Switch to the new branch.
"""
repository = Repository()

if repository.head.ref != repository.heads.master:
error(
f"working copy is from non-master branch: {repository.head.ref}", 1
)

if repository.is_dirty():
warning("working copy is dirty")

version = currentVersion()

if version.release_candidate is not None:
error(f"current version is already a release candidate: {version}", 1)

incrementVersion(candidate=True)
version = currentVersion()

print(f"New release candidate version: {version.public()}")

branch = createReleaseBranch(repository, version)
branch.checkout()

print("Next steps (to be done manually):")
print(" • Commit version changes to the new release branch:", branch)
print(" • Push the release branch to GitHub")
print(" • Open a pull request on GitHub from the release branch")


def bumpRelease() -> None:
"""
Increment the release candidate version.
"""
repository = Repository()

if repository.is_dirty():
warning("working copy is dirty")

version = currentVersion()

if version.release_candidate is None:
error(f"current version is not a release candidate: {version}", 1)

branch = releaseBranch(repository, version)

if repository.head.ref != branch:
error(
f'working copy is on branch "{repository.head.ref}", '
f'not release branch "{branch}"',
1,
)

incrementVersion(candidate=True)
version = currentVersion()

print("New release candidate version:", version.public())


def publishRelease(final: bool, test: bool = False) -> None:
"""
Publish the current version.
"""
repository = Repository()

if repository.is_dirty():
error("working copy is dirty", 1)

version = currentVersion()

if version.release_candidate is None:
error(f"current version is not a release candidate: {version}", 1)

branch = releaseBranch(repository, version)

if repository.head.ref != branch:
error(
f'working copy is on branch "{repository.head.ref}", '
f'not release branch "{branch}"',
1,
)

incrementVersion(candidate=False)
version = currentVersion()

versonFile = Path(__file__).parent / "src" / "klein" / "_version.py"
repository.index.add(str(versonFile))
repository.index.commit(f"Update version to {version}")

tagName = releaseTagName(version)

if tagName in repository.tags:
tag = repository.tags[tagName]
message = f"Release tag already exists: {tagName}"
if tag.commit != repository.head.ref.commit:
error(message, 1)
else:
print(message)
else:
print("Creating release tag:", tagName)
tag = repository.create_tag(
tagName, ref=branch, message=f"Tag release {version.public()}"
)

print("Pushing tag to origin:", tag)
repository.remotes.origin.push(refspec=tag.path)

distribute(repository, tag, test=test)


@commandGroup()
def main() -> None:
pass


@main.command()
def start() -> None:
startRelease()


@main.command()
def bump() -> None:
bumpRelease()


@main.command()
@commandOption("--test/--production")
@commandOption("--final/--candidate")
def publish(final: bool, test: bool) -> None:
publishRelease(final=final, test=test)


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: Implementation :: PyPy',
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
Expand Down
Loading