From 4837c74c6a5d9523c6a3eed22a48d277b1d016b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Thu, 2 Apr 2020 10:45:26 -0700 Subject: [PATCH 01/31] Add a script to automate some of the release process. --- docs/release.rst | 9 +-- release.py | 148 +++++++++++++++++++++++++++++++++++++++++++++++ tox.ini | 36 ++++++++++-- 3 files changed, 185 insertions(+), 8 deletions(-) create mode 100644 release.py diff --git a/docs/release.rst b/docs/release.rst index d137ddd3..83d72baa 100644 --- a/docs/release.rst +++ b/docs/release.rst @@ -8,13 +8,14 @@ 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-" -#. Run :code:`python incremental.update Klein --rc && python incremental.update Klein` -#. Commit, and push the branch +#. 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 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 -m "Tag release"`` diff --git a/release.py b/release.py new file mode 100644 index 00000000..aa3d10d8 --- /dev/null +++ b/release.py @@ -0,0 +1,148 @@ +from pathlib import Path +from subprocess import CalledProcessError, run +from sys import exit, stderr +from typing import Any, Dict, NoReturn, Optional, Sequence + +from git import Repo as Repository +from git.refs.head import Head + +from incremental import Version + + +def warning(message: str) -> None: + print(f"WARNING: {message}", file=stderr) + + +def error(message: str, exitStatus: int) -> NoReturn: + print(f"ERROR: {message}", file=stderr) + exit(exitStatus) + + +def spawn(args: Sequence[str]) -> None: + run(args, capture_output=True, check=True) + + +def currentVersion() -> Version: + versionInfo: Dict[str, Any] = {} + versonFile = Path(__file__).parent / "src" / "klein" / "_version.py" + exec (versonFile.read_text(), versionInfo) + return versionInfo["__version__"] + + +def incrementVersion(candidate: bool) -> None: + # 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") + try: + spawn(args) + except CalledProcessError as e: + error(f"command {e.cmd} failed: {e.stderr}", 1) + + +def releaseBranchName(version: Version) -> str: + return f"release-{version.major}.{version.minor}" + + +def releaseBranch(repository: Repository, version: Version) -> Optional[Head]: + branchName = releaseBranchName(version) + + if branchName in repository.heads: + return repository.heads[branchName] + + return None + + +def createReleaseBranch(repository: Repository, version: Version) -> Head: + 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 startRelease() -> None: + 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}") + + branch = createReleaseBranch(repository, version) + branch.checkout() + + print( + ( + f"Next steps:\n" + f" • Commit version updates to release branch: {branch}\n" + f" • Push the release branch to GitHub\n" + f" • Open a pull request on GitHub from the release branch\n" + ), + end="", + ) + + +def bumpRelease() -> None: + 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) + + incrementVersion(candidate=True) + version = currentVersion() + + print(f"New release candidate version: {version}") + + 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, + ) + + +def main(argv: Sequence[str]) -> None: + def invalidArguments() -> NoReturn: + error(f"invalid arguments: {argv}", 64) + + if len(argv) != 1: + invalidArguments() + + subcommand = argv[0] + + if subcommand == "start": + startRelease() + elif subcommand == "bump": + bumpRelease() + else: + invalidArguments() + + +if __name__ == "__main__": + from sys import argv + + main(argv[1:]) diff --git a/tox.ini b/tox.ini index 3cc06f62..15ac857e 100644 --- a/tox.ini +++ b/tox.ini @@ -111,7 +111,7 @@ setenv = BLACK_LINT_ARGS=--check commands = - black {env:BLACK_LINT_ARGS:} src + black {env:BLACK_LINT_ARGS:} {posargs:release.py src} [testenv:black-reformat] @@ -150,7 +150,7 @@ deps = git+git://github.com/PyCQA/pyflakes@ffe9386#egg=pyflakes commands = - flake8 {posargs:src/{env:PY_MODULE}} + flake8 {posargs:release.py setup.py src/{env:PY_MODULE}} [flake8] @@ -275,7 +275,7 @@ commands = --config-file="{toxinidir}/tox.ini" \ --cache-dir="{toxworkdir}/mypy_cache" \ {tty:--pretty:} \ - {posargs:src} + {posargs:release.py setup.py src} [mypy] @@ -338,7 +338,7 @@ allow_untyped_defs = True [mypy-constantly] ignore_missing_imports = True -[mypy-hyperlink] +[mypy-hyperlink] ignore_missing_imports = True [mypy-incremental] @@ -367,6 +367,10 @@ ignore_missing_imports = True [mypy-twisted.*] ignore_missing_imports = True +[mypy-git] +[mypy-git.*] +ignore_missing_imports = True + ## # Coverage report @@ -535,3 +539,27 @@ deps = commands = pip freeze + + +## +# Release +## + +[testenv:release] + +description = create a prerelease branch + +basepython = {[default]basepython} + +skip_install = True + +deps = + incremental[scripts]==17.5.0 + GitPython==3.1.0 + +whitelist_externals = + git + echo + +commands = + python "{toxinidir}/release.py" {posargs} From 52347231c7ff5a7e5ee15a1f2f1f6095ca5810d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Thu, 2 Apr 2020 10:45:53 -0700 Subject: [PATCH 02/31] Update to 20.4.0rc1 --- src/klein/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/klein/_version.py b/src/klein/_version.py index 128af21a..2ee98a45 100644 --- a/src/klein/_version.py +++ b/src/klein/_version.py @@ -7,5 +7,5 @@ from incremental import Version -__version__ = Version("klein", 19, 6, 0) +__version__ = Version('klein', 20, 4, 0, release_candidate=1) __all__ = ["__version__"] From 0d0d0a606260b50a7f569dcec57ef63d15430777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Thu, 2 Apr 2020 11:23:40 -0700 Subject: [PATCH 03/31] Ignore E211 here because black has a different opinion WRT exec. --- release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release.py b/release.py index aa3d10d8..edbcbdbd 100644 --- a/release.py +++ b/release.py @@ -25,7 +25,7 @@ def spawn(args: Sequence[str]) -> None: def currentVersion() -> Version: versionInfo: Dict[str, Any] = {} versonFile = Path(__file__).parent / "src" / "klein" / "_version.py" - exec (versonFile.read_text(), versionInfo) + exec (versonFile.read_text(), versionInfo) # noqa: E211 # black py2.7 return versionInfo["__version__"] From 0415f73c26ee80f1cd0c8319226f4ec7b998ac6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Thu, 2 Apr 2020 11:24:06 -0700 Subject: [PATCH 04/31] Don't puke on lack of type hints from setuptools --- tox.ini | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/tox.ini b/tox.ini index 15ac857e..5f475e43 100644 --- a/tox.ini +++ b/tox.ini @@ -338,27 +338,30 @@ allow_untyped_defs = True [mypy-constantly] ignore_missing_imports = True -[mypy-hyperlink] +[mypy-git] +[mypy-git.*] ignore_missing_imports = True -[mypy-incremental] +[mypy-hyperlink] ignore_missing_imports = True -[mypy-zope.interface] -[mypy-zope.interface.*] +[mypy-hypothesis] ignore_missing_imports = True - -[mypy-treq] +[mypy-hypothesis.*] ignore_missing_imports = True -[mypy-treq.*] + +[mypy-idna] ignore_missing_imports = True -[mypy-hypothesis] +[mypy-incremental] ignore_missing_imports = True -[mypy-hypothesis.*] + +[mypy-setuptools.*] ignore_missing_imports = True -[mypy-idna] +[mypy-treq] +ignore_missing_imports = True +[mypy-treq.*] ignore_missing_imports = True [mypy-tubes.*] @@ -367,8 +370,8 @@ ignore_missing_imports = True [mypy-twisted.*] ignore_missing_imports = True -[mypy-git] -[mypy-git.*] +[mypy-zope.interface] +[mypy-zope.interface.*] ignore_missing_imports = True From 8767e860548e5618cc53e65136e7eb2cf8bdf662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Thu, 2 Apr 2020 11:24:49 -0700 Subject: [PATCH 05/31] Incremental doesn't produce black-compatible output. --- src/klein/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/klein/_version.py b/src/klein/_version.py index 2ee98a45..c22b1e2e 100644 --- a/src/klein/_version.py +++ b/src/klein/_version.py @@ -7,5 +7,5 @@ from incremental import Version -__version__ = Version('klein', 20, 4, 0, release_candidate=1) +__version__ = Version("klein", 20, 4, 0, release_candidate=1) __all__ = ["__version__"] From 735c6ef467a38516a1da3f2f948d0c0130ab94a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Thu, 2 Apr 2020 14:12:54 -0700 Subject: [PATCH 06/31] Remove coverage-py27-twtrunk, which will no longer succeed. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a50524f3..1ab824ee 100644 --- a/.travis.yml +++ b/.travis.yml @@ -91,7 +91,6 @@ matrix: allow_failures: # Tests against Twisted trunk are allow to fail, as they are not supported. - - env: TOXENV=coverage-py27-twtrunk,codecov - env: TOXENV=coverage-py38-twtrunk,codecov # This depends on external web sites, so it's allowed to fail. From a7ddee5b5c6aa5dc16b40da25ad737ad2c28c96c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Thu, 2 Apr 2020 14:57:11 -0700 Subject: [PATCH 07/31] Add some docstrings. --- release.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/release.py b/release.py index edbcbdbd..a8f67785 100644 --- a/release.py +++ b/release.py @@ -10,19 +10,34 @@ 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{CalledProcessError} + with captured output if the exit status is non-zero. + """ run(args, capture_output=True, check=True) 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 @@ -30,6 +45,11 @@ def currentVersion() -> Version: 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"] @@ -42,10 +62,16 @@ def incrementVersion(candidate: bool) -> None: 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: @@ -55,6 +81,9 @@ def releaseBranch(repository: Repository, version: Version) -> Optional[Head]: def createReleaseBranch(repository: Repository, version: Version) -> Head: + """ + Create a new release branch. + """ branchName = releaseBranchName(version) if branchName in repository.heads: @@ -65,6 +94,12 @@ def createReleaseBranch(repository: Repository, version: Version) -> Head: 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: @@ -100,6 +135,9 @@ def startRelease() -> None: def bumpRelease() -> None: + """ + Increment the release candidate version. + """ repository = Repository() if repository.is_dirty(): @@ -125,7 +163,17 @@ def bumpRelease() -> None: ) +def publishRelease() -> None: + """ + Publish the current version. + """ + raise NotImplementedError() + + def main(argv: Sequence[str]) -> None: + """ + Command line entry point. + """ def invalidArguments() -> NoReturn: error(f"invalid arguments: {argv}", 64) @@ -138,6 +186,8 @@ def invalidArguments() -> NoReturn: startRelease() elif subcommand == "bump": bumpRelease() + elif subcommand == "publish": + publishRelease() else: invalidArguments() From fd5e16b1ccffdc3b7a1361f77e4a34f37516d19b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Thu, 2 Apr 2020 14:58:45 -0700 Subject: [PATCH 08/31] Get rid of coverage-py27-twtrunk harder --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1ab824ee..ede7c947 100644 --- a/.travis.yml +++ b/.travis.yml @@ -79,8 +79,6 @@ matrix: # Test against Twisted trunk in case something in development breaks us. # This is allowed to fail below, since the bug may be in Twisted. - - python: 2.7 - env: TOXENV=coverage-py27-twtrunk,codecov - python: 3.8 env: TOXENV=coverage-py38-twtrunk,codecov From 4348b96f209cbd4fae9ada71ca8c21daabbbf95f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Thu, 2 Apr 2020 16:21:01 -0700 Subject: [PATCH 09/31] Check branch prior to bumping version --- release.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/release.py b/release.py index a8f67785..bdcef956 100644 --- a/release.py +++ b/release.py @@ -148,11 +148,6 @@ def bumpRelease() -> None: if version.release_candidate is None: error(f"current version is not a release candidate: {version}", 1) - incrementVersion(candidate=True) - version = currentVersion() - - print(f"New release candidate version: {version}") - branch = releaseBranch(repository, version) if repository.head.ref != branch: @@ -162,6 +157,11 @@ def bumpRelease() -> None: 1, ) + incrementVersion(candidate=True) + version = currentVersion() + + print(f"New release candidate version: {version}") + def publishRelease() -> None: """ From f52a4430f22cb97f1f70689ec89617d7ad86787f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Thu, 2 Apr 2020 16:21:19 -0700 Subject: [PATCH 10/31] Run black-reformat after incrementing version --- release.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/release.py b/release.py index bdcef956..de9b37a6 100644 --- a/release.py +++ b/release.py @@ -29,7 +29,10 @@ def spawn(args: Sequence[str]) -> None: Spawn a new process with the given arguments, raising L{CalledProcessError} with captured output if the exit status is non-zero. """ - run(args, capture_output=True, check=True) + try: + run(args, capture_output=True, check=True) + except CalledProcessError as e: + error(f"command {e.cmd} failed: {e.stderr}", 1) def currentVersion() -> Version: @@ -44,6 +47,13 @@ def currentVersion() -> Version: return versionInfo["__version__"] +def fadeToBlack(): + """ + Run black to reformat the source code. + """ + spawn(["tox", "-e", "black-reformat"]) + + def incrementVersion(candidate: bool) -> None: """ Increment the current release version. @@ -55,10 +65,10 @@ def incrementVersion(candidate: bool) -> None: args = ["python", "-m", "incremental.update", "klein"] if candidate: args.append("--rc") - try: - spawn(args) - except CalledProcessError as e: - error(f"command {e.cmd} failed: {e.stderr}", 1) + spawn(args) + + # Incremental generates code that black wants to reformat. + fadeToBlack() def releaseBranchName(version: Version) -> str: @@ -174,6 +184,7 @@ def main(argv: Sequence[str]) -> None: """ Command line entry point. """ + def invalidArguments() -> NoReturn: error(f"invalid arguments: {argv}", 64) From c70570fa98dee9ce4bf7ef3d66ccb386a83102fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Mon, 6 Apr 2020 14:41:52 -0700 Subject: [PATCH 11/31] Missing return type --- release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release.py b/release.py index de9b37a6..d46f249a 100644 --- a/release.py +++ b/release.py @@ -47,7 +47,7 @@ def currentVersion() -> Version: return versionInfo["__version__"] -def fadeToBlack(): +def fadeToBlack() -> None: """ Run black to reformat the source code. """ From 45e68d7323909f0165c88b60b18f9baf29920d53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Thu, 21 May 2020 17:20:01 -0700 Subject: [PATCH 12/31] use click --- release.py | 37 +++++++++++++++++-------------------- tox.ini | 1 + 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/release.py b/release.py index d46f249a..183c0af6 100644 --- a/release.py +++ b/release.py @@ -3,6 +3,8 @@ from sys import exit, stderr from typing import Any, Dict, NoReturn, Optional, Sequence +from click import command, group as commandGroup + from git import Repo as Repository from git.refs.head import Head @@ -180,30 +182,25 @@ def publishRelease() -> None: raise NotImplementedError() -def main(argv: Sequence[str]) -> None: - """ - Command line entry point. - """ +@commandGroup() +def main() -> None: + pass - def invalidArguments() -> NoReturn: - error(f"invalid arguments: {argv}", 64) - if len(argv) != 1: - invalidArguments() +@main.command() +def start() -> None: + startRelease() - subcommand = argv[0] - if subcommand == "start": - startRelease() - elif subcommand == "bump": - bumpRelease() - elif subcommand == "publish": - publishRelease() - else: - invalidArguments() +@main.command() +def bump() -> None: + bumpRelease() -if __name__ == "__main__": - from sys import argv +@main.command() +def publish() -> None: + publishRelease() + - main(argv[1:]) +if __name__ == "__main__": + main() diff --git a/tox.ini b/tox.ini index eb2915f3..0b981f14 100644 --- a/tox.ini +++ b/tox.ini @@ -582,6 +582,7 @@ skip_install = True deps = incremental[scripts]==17.5.0 GitPython==3.1.0 + click==7.1.2 whitelist_externals = git From ca9640d5aab55653124b1555c7b58d4e7421b73f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Fri, 22 May 2020 11:01:31 -0700 Subject: [PATCH 13/31] publishRelease() now tags the repo and pushes the tag --- docs/release.rst | 7 +++---- release.py | 45 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/docs/release.rst b/docs/release.rst index 83d72baa..a70f9d33 100644 --- a/docs/release.rst +++ b/docs/release.rst @@ -14,12 +14,11 @@ Releasing 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 merging a PR). +#. 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: :code:`tox -e release publish` -#. 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 -m "Tag 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``. diff --git a/release.py b/release.py index 183c0af6..dcab2ddc 100644 --- a/release.py +++ b/release.py @@ -92,6 +92,13 @@ def releaseBranch(repository: Repository, version: Version) -> Optional[Head]: 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. @@ -130,7 +137,7 @@ def startRelease() -> None: incrementVersion(candidate=True) version = currentVersion() - print(f"New release candidate version: {version}") + print(f"New release candidate version: {version.public()}") branch = createReleaseBranch(repository, version) branch.checkout() @@ -172,14 +179,46 @@ def bumpRelease() -> None: incrementVersion(candidate=True) version = currentVersion() - print(f"New release candidate version: {version}") + print(f"New release candidate version: {version.public()}") def publishRelease() -> None: """ Publish the current version. """ - raise NotImplementedError() + 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, + ) + + tagName = releaseTagName(version) + + if tagName in repository.tags: + tag = repository.tags[tagName] + if tag.commit != repository.head.ref.commit: + error(f"Release tag already exists: {tagName}", 1) + 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) @commandGroup() From 91d15ed7d303313c969a17f20801478e5a697a23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Fri, 22 May 2020 16:04:41 -0700 Subject: [PATCH 14/31] Don't need whitelist_externals, pass SSH_AUTH_SOCK through. --- tox.ini | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 0b981f14..91aaef66 100644 --- a/tox.ini +++ b/tox.ini @@ -584,9 +584,8 @@ deps = GitPython==3.1.0 click==7.1.2 -whitelist_externals = - git - echo +passenv = + SSH_AUTH_SOCK commands = python "{toxinidir}/release.py" {posargs} From c20ba85fde129f6ff734917a3d1950a948a5e947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Tue, 26 May 2020 10:38:09 -0700 Subject: [PATCH 15/31] Add publish --- docs/release.rst | 9 +---- release.py | 101 ++++++++++++++++++++++++++++++++++++++--------- tox.ini | 5 ++- 3 files changed, 88 insertions(+), 27 deletions(-) diff --git a/docs/release.rst b/docs/release.rst index a70f9d33..c7c4ab79 100644 --- a/docs/release.rst +++ b/docs/release.rst @@ -16,10 +16,5 @@ Releasing Klein #. 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: :code:`tox -e release publish` - -#. Clear the directory of any other changes using ``git clean -f -x -d .`` -#. 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-*``. +#. To publish a release candidate to PyPI: :code:`tox -e release publish` (add :code:`--test` to submit to TestPyPI instead) +#. To publish a production release: :code:`tox -e release publish --final` diff --git a/release.py b/release.py index dcab2ddc..873ee1ba 100644 --- a/release.py +++ b/release.py @@ -1,16 +1,25 @@ +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 command, group as commandGroup +from click import group as commandGroup, option as commandOption -from git import Repo as Repository +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. @@ -31,8 +40,9 @@ def spawn(args: Sequence[str]) -> None: Spawn a new process with the given arguments, raising L{CalledProcessError} with captured output if the exit status is non-zero. """ + print("Executing command:", " ".join(repr(arg) for arg in args)) try: - run(args, capture_output=True, check=True) + run(args, input=b"", capture_output=True, check=True) except CalledProcessError as e: error(f"command {e.cmd} failed: {e.stderr}", 1) @@ -112,6 +122,56 @@ def createReleaseBranch(repository: Repository, version: Version) -> Head: 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: @@ -142,15 +202,10 @@ def startRelease() -> None: branch = createReleaseBranch(repository, version) branch.checkout() - print( - ( - f"Next steps:\n" - f" • Commit version updates to release branch: {branch}\n" - f" • Push the release branch to GitHub\n" - f" • Open a pull request on GitHub from the release branch\n" - ), - end="", - ) + 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: @@ -179,17 +234,17 @@ def bumpRelease() -> None: incrementVersion(candidate=True) version = currentVersion() - print(f"New release candidate version: {version.public()}") + print("New release candidate version:", version.public()) -def publishRelease() -> None: +def publishRelease(final: bool, test: bool = False) -> None: """ Publish the current version. """ repository = Repository() if repository.is_dirty(): - warning("working copy is dirty") + error("working copy is dirty", 1) version = currentVersion() @@ -205,12 +260,18 @@ def publishRelease() -> None: 1, ) + incrementVersion(candidate=False) + version = currentVersion() + 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(f"Release tag already exists: {tagName}", 1) + error(message, 1) + else: + print(message) else: print("Creating release tag:", tagName) tag = repository.create_tag( @@ -220,6 +281,8 @@ def publishRelease() -> None: print("Pushing tag to origin:", tag) repository.remotes.origin.push(refspec=tag.path) + distribute(repository, tag, test=test) + @commandGroup() def main() -> None: @@ -237,8 +300,10 @@ def bump() -> None: @main.command() -def publish() -> None: - publishRelease() +@commandOption("--test/--production") +@commandOption("--final/--candidate") +def publish(final: bool, test: bool) -> None: + publishRelease(final=final, test=test) if __name__ == "__main__": diff --git a/tox.ini b/tox.ini index 91aaef66..5ffef22d 100644 --- a/tox.ini +++ b/tox.ini @@ -580,9 +580,10 @@ basepython = {[default]basepython} skip_install = True deps = - incremental[scripts]==17.5.0 - GitPython==3.1.0 click==7.1.2 + GitPython==3.1.0 + incremental[scripts]==17.5.0 + twine==3.1.1 passenv = SSH_AUTH_SOCK From c8b9705a31af55d72da4980b7759652267448d0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Tue, 26 May 2020 10:42:15 -0700 Subject: [PATCH 16/31] Clean up docs. --- docs/release.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/release.rst b/docs/release.rst index c7c4ab79..1806da0d 100644 --- a/docs/release.rst +++ b/docs/release.rst @@ -16,5 +16,7 @@ Releasing Klein #. 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` (add :code:`--test` to submit to TestPyPI instead) -#. To publish a production release: :code:`tox -e release publish --final` +#. 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. From d14ca5d1417b5a3b201bc05e5a94109b85ac29a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Tue, 26 May 2020 10:46:09 -0700 Subject: [PATCH 17/31] Doc tweak --- docs/release.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release.rst b/docs/release.rst index 1806da0d..2efe4d7a 100644 --- a/docs/release.rst +++ b/docs/release.rst @@ -12,7 +12,7 @@ Releasing Klein --------------- #. Start with a clean (no changes) source tree on the master branch. -#. Create a new release candidate: :code:`tox -e release start` +#. 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) From c2731264cab81239584df051e4137ec0cc38908e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Tue, 26 May 2020 10:50:11 -0700 Subject: [PATCH 18/31] Let's go ahead and call this 20.6 instead --- src/klein/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/klein/_version.py b/src/klein/_version.py index c22b1e2e..9b43bd48 100644 --- a/src/klein/_version.py +++ b/src/klein/_version.py @@ -7,5 +7,5 @@ from incremental import Version -__version__ = Version("klein", 20, 4, 0, release_candidate=1) +__version__ = Version("klein", 20, 6, 0, release_candidate=1) __all__ = ["__version__"] From 2cd808165e9eff9c796e25b4fdde75a891787a83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Tue, 26 May 2020 10:57:55 -0700 Subject: [PATCH 19/31] Simplify diff --- tox.ini | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/tox.ini b/tox.ini index 5ffef22d..58822a7d 100644 --- a/tox.ini +++ b/tox.ini @@ -341,30 +341,33 @@ allow_untyped_defs = True [mypy-constantly] ignore_missing_imports = True -[mypy-git] [mypy-git.*] ignore_missing_imports = True [mypy-hyperlink] ignore_missing_imports = True -[mypy-hypothesis] -ignore_missing_imports = True -[mypy-hypothesis.*] +[mypy-incremental] ignore_missing_imports = True -[mypy-idna] +[mypy-zope.interface] +[mypy-zope.interface.*] ignore_missing_imports = True -[mypy-incremental] +[mypy-treq] +ignore_missing_imports = True +[mypy-treq.*] ignore_missing_imports = True -[mypy-setuptools.*] +[mypy-hypothesis] +ignore_missing_imports = True +[mypy-hypothesis.*] ignore_missing_imports = True -[mypy-treq] +[mypy-idna] ignore_missing_imports = True -[mypy-treq.*] + +[mypy-setuptools] ignore_missing_imports = True [mypy-tubes.*] @@ -373,10 +376,6 @@ ignore_missing_imports = True [mypy-twisted.*] ignore_missing_imports = True -[mypy-zope.interface] -[mypy-zope.interface.*] -ignore_missing_imports = True - ## # Coverage report From e84729e29710c3a76830895200cd367f1c404c8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Wed, 27 May 2020 09:27:48 -0700 Subject: [PATCH 20/31] Annotate support for Python 3.8 --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index fe1bb57a..50370c5b 100644 --- a/setup.py +++ b/setup.py @@ -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', From b60cbe22c3481ec2f3cfca314b252d43205b819c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Wed, 3 Jun 2020 11:52:48 -0700 Subject: [PATCH 21/31] Capture some news for this release. --- NEWS.rst | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/NEWS.rst b/NEWS.rst index 15dc7934..f5f513eb 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,6 +1,36 @@ NEWS ==== +20.6.0 - 2020-06-?? +------------------- + * This is the last release of Klein expected to support Python 2. + * Python 3.8 is now supported by Klein. [`#303 `_] + * Python 3.4 is no longer supported by Klein. [`#284 `_] + * ``klein.app.subroute`` is now also available as ``klein.subroute``. [`#293 `_] + * Support for forms and sessions. [`#276 `_] + * The ``Klein`` class now supports deep copy by implementing ``__copy__``. [`#74 `_] + +19.6.0 - 2019-06-07 +------------------- + +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 `_] From e50767c6516f9dbb1035d78d7020dab5ba934733 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Sun, 7 Jun 2020 11:21:12 -0700 Subject: [PATCH 22/31] Add copyright --- release.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/release.py b/release.py index 873ee1ba..151b9d3e 100644 --- a/release.py +++ b/release.py @@ -1,3 +1,6 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + from enum import Enum from os import chdir from pathlib import Path From e256bed61e0b3c34d53ec64b2ded3b4ad2d41e23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Sun, 7 Jun 2020 11:23:54 -0700 Subject: [PATCH 23/31] Fix docstring --- release.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/release.py b/release.py index 151b9d3e..3c67636d 100644 --- a/release.py +++ b/release.py @@ -40,8 +40,8 @@ def error(message: str, exitStatus: int) -> NoReturn: def spawn(args: Sequence[str]) -> None: """ - Spawn a new process with the given arguments, raising L{CalledProcessError} - with captured output if the exit status is non-zero. + 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: From 955c0162c059c265d112c13b111dc997687423af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Sun, 7 Jun 2020 11:24:14 -0700 Subject: [PATCH 24/31] Not imminent dropping for Python 3.5. --- NEWS.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NEWS.rst b/NEWS.rst index f5f513eb..2d257115 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -4,8 +4,9 @@ NEWS 20.6.0 - 2020-06-?? ------------------- * This is the last release of Klein expected to support Python 2. - * Python 3.8 is now supported by Klein. [`#303 `_] + * This is the last release of Klein expected to support Python 3.5. * Python 3.4 is no longer supported by Klein. [`#284 `_] + * Python 3.8 is now supported by Klein. [`#303 `_] * ``klein.app.subroute`` is now also available as ``klein.subroute``. [`#293 `_] * Support for forms and sessions. [`#276 `_] * The ``Klein`` class now supports deep copy by implementing ``__copy__``. [`#74 `_] From 6a04b249cf528ec99e75e9b77e5e3a044d1e67ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Sun, 7 Jun 2020 11:24:35 -0700 Subject: [PATCH 25/31] Fix description for release environment --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index ddea2349..45ae6875 100644 --- a/tox.ini +++ b/tox.ini @@ -572,7 +572,7 @@ commands = [testenv:release] -description = create a prerelease branch +description = invoke tool to manage a release branch basepython = {[default]basepython} From 11edde492f6d43a9b011c352f7ef625db25d3ecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Sun, 7 Jun 2020 19:14:33 -0700 Subject: [PATCH 26/31] Note forms & sessions added. --- NEWS.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS.rst b/NEWS.rst index 2d257115..01146164 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -14,6 +14,8 @@ NEWS 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 -------------------- From 16b4ff81b5f5e54ea98b0833a1c2bf25b3450a60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Sun, 7 Jun 2020 19:17:17 -0700 Subject: [PATCH 27/31] Set release date --- NEWS.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.rst b/NEWS.rst index 01146164..4eb7a724 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,7 +1,7 @@ NEWS ==== -20.6.0 - 2020-06-?? +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. From a1f597a1e7b08d9ae8bdcccecae783bf79a30abe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Sun, 7 Jun 2020 19:46:47 -0700 Subject: [PATCH 28/31] Update version to [klein, version 20.6.0] --- src/klein/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/klein/_version.py b/src/klein/_version.py index 9b43bd48..8f6e0946 100644 --- a/src/klein/_version.py +++ b/src/klein/_version.py @@ -7,5 +7,5 @@ from incremental import Version -__version__ = Version("klein", 20, 6, 0, release_candidate=1) +__version__ = Version("klein", 20, 6, 0) __all__ = ["__version__"] From 04a7048f7107ab28a157fb61fbf041af6863726f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Sun, 7 Jun 2020 19:50:44 -0700 Subject: [PATCH 29/31] Revert "Update version to [klein, version 20.6.0]" This reverts commit a1f597a1e7b08d9ae8bdcccecae783bf79a30abe. --- src/klein/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/klein/_version.py b/src/klein/_version.py index 8f6e0946..9b43bd48 100644 --- a/src/klein/_version.py +++ b/src/klein/_version.py @@ -7,5 +7,5 @@ from incremental import Version -__version__ = Version("klein", 20, 6, 0) +__version__ = Version("klein", 20, 6, 0, release_candidate=1) __all__ = ["__version__"] From 6dbe083954d9a876002c845e062374bd520f97bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Sun, 7 Jun 2020 19:51:16 -0700 Subject: [PATCH 30/31] Commit after updating final version --- release.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/release.py b/release.py index 3c67636d..b87c01c1 100644 --- a/release.py +++ b/release.py @@ -266,6 +266,10 @@ def publishRelease(final: bool, test: bool = False) -> None: 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: From 699d7b8b32c18ac15b5d10057241de2585c67c9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilfredo=20Sa=CC=81nchez?= Date: Sun, 7 Jun 2020 19:52:04 -0700 Subject: [PATCH 31/31] Update version to [klein, version 20.6.0] --- src/klein/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/klein/_version.py b/src/klein/_version.py index 9b43bd48..8f6e0946 100644 --- a/src/klein/_version.py +++ b/src/klein/_version.py @@ -7,5 +7,5 @@ from incremental import Version -__version__ = Version("klein", 20, 6, 0, release_candidate=1) +__version__ = Version("klein", 20, 6, 0) __all__ = ["__version__"]