|
|
@ -25,7 +25,7 @@ import sys |
|
|
|
import urllib.request |
|
|
|
import urllib.request |
|
|
|
from os import path |
|
|
|
from os import path |
|
|
|
from tempfile import TemporaryDirectory |
|
|
|
from tempfile import TemporaryDirectory |
|
|
|
from typing import List, Optional |
|
|
|
from typing import Any, List, Optional, cast |
|
|
|
|
|
|
|
|
|
|
|
import attr |
|
|
|
import attr |
|
|
|
import click |
|
|
|
import click |
|
|
@ -36,7 +36,9 @@ from github import Github |
|
|
|
from packaging import version |
|
|
|
from packaging import version |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_until_successful(command, *args, **kwargs): |
|
|
|
def run_until_successful( |
|
|
|
|
|
|
|
command: str, *args: Any, **kwargs: Any |
|
|
|
|
|
|
|
) -> subprocess.CompletedProcess: |
|
|
|
while True: |
|
|
|
while True: |
|
|
|
completed_process = subprocess.run(command, *args, **kwargs) |
|
|
|
completed_process = subprocess.run(command, *args, **kwargs) |
|
|
|
exit_code = completed_process.returncode |
|
|
|
exit_code = completed_process.returncode |
|
|
@ -50,7 +52,7 @@ def run_until_successful(command, *args, **kwargs): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@click.group() |
|
|
|
@click.group() |
|
|
|
def cli(): |
|
|
|
def cli() -> None: |
|
|
|
"""An interactive script to walk through the parts of creating a release. |
|
|
|
"""An interactive script to walk through the parts of creating a release. |
|
|
|
|
|
|
|
|
|
|
|
Requires the dev dependencies be installed, which can be done via: |
|
|
|
Requires the dev dependencies be installed, which can be done via: |
|
|
@ -81,7 +83,7 @@ def cli(): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@cli.command() |
|
|
|
@cli.command() |
|
|
|
def prepare(): |
|
|
|
def prepare() -> None: |
|
|
|
"""Do the initial stages of creating a release, including creating release |
|
|
|
"""Do the initial stages of creating a release, including creating release |
|
|
|
branch, updating changelog and pushing to GitHub. |
|
|
|
branch, updating changelog and pushing to GitHub. |
|
|
|
""" |
|
|
|
""" |
|
|
@ -161,7 +163,9 @@ def prepare(): |
|
|
|
click.get_current_context().abort() |
|
|
|
click.get_current_context().abort() |
|
|
|
|
|
|
|
|
|
|
|
# Switch to the release branch. |
|
|
|
# Switch to the release branch. |
|
|
|
parsed_new_version: version.Version = version.parse(new_version) |
|
|
|
# Cast safety: parse() won't return a version.LegacyVersion from our |
|
|
|
|
|
|
|
# version string format. |
|
|
|
|
|
|
|
parsed_new_version = cast(version.Version, version.parse(new_version)) |
|
|
|
|
|
|
|
|
|
|
|
# We assume for debian changelogs that we only do RCs or full releases. |
|
|
|
# We assume for debian changelogs that we only do RCs or full releases. |
|
|
|
assert not parsed_new_version.is_devrelease |
|
|
|
assert not parsed_new_version.is_devrelease |
|
|
@ -176,7 +180,6 @@ def prepare(): |
|
|
|
# If the release branch only exists on the remote we check it out |
|
|
|
# If the release branch only exists on the remote we check it out |
|
|
|
# locally. |
|
|
|
# locally. |
|
|
|
repo.git.checkout(release_branch_name) |
|
|
|
repo.git.checkout(release_branch_name) |
|
|
|
release_branch = repo.active_branch |
|
|
|
|
|
|
|
else: |
|
|
|
else: |
|
|
|
# If a branch doesn't exist we create one. We ask which one branch it |
|
|
|
# If a branch doesn't exist we create one. We ask which one branch it |
|
|
|
# should be based off, defaulting to sensible values depending on the |
|
|
|
# should be based off, defaulting to sensible values depending on the |
|
|
@ -198,13 +201,15 @@ def prepare(): |
|
|
|
click.get_current_context().abort() |
|
|
|
click.get_current_context().abort() |
|
|
|
|
|
|
|
|
|
|
|
# Check out the base branch and ensure it's up to date |
|
|
|
# Check out the base branch and ensure it's up to date |
|
|
|
repo.head.reference = base_branch |
|
|
|
repo.head.set_reference(base_branch, "check out the base branch") |
|
|
|
repo.head.reset(index=True, working_tree=True) |
|
|
|
repo.head.reset(index=True, working_tree=True) |
|
|
|
if not base_branch.is_remote(): |
|
|
|
if not base_branch.is_remote(): |
|
|
|
update_branch(repo) |
|
|
|
update_branch(repo) |
|
|
|
|
|
|
|
|
|
|
|
# Create the new release branch |
|
|
|
# Create the new release branch |
|
|
|
release_branch = repo.create_head(release_branch_name, commit=base_branch) |
|
|
|
# Type ignore will no longer be needed after GitPython 3.1.28. |
|
|
|
|
|
|
|
# See https://github.com/gitpython-developers/GitPython/pull/1419 |
|
|
|
|
|
|
|
repo.create_head(release_branch_name, commit=base_branch) # type: ignore[arg-type] |
|
|
|
|
|
|
|
|
|
|
|
# Switch to the release branch and ensure it's up to date. |
|
|
|
# Switch to the release branch and ensure it's up to date. |
|
|
|
repo.git.checkout(release_branch_name) |
|
|
|
repo.git.checkout(release_branch_name) |
|
|
@ -265,7 +270,7 @@ def prepare(): |
|
|
|
|
|
|
|
|
|
|
|
@cli.command() |
|
|
|
@cli.command() |
|
|
|
@click.option("--gh-token", envvar=["GH_TOKEN", "GITHUB_TOKEN"]) |
|
|
|
@click.option("--gh-token", envvar=["GH_TOKEN", "GITHUB_TOKEN"]) |
|
|
|
def tag(gh_token: Optional[str]): |
|
|
|
def tag(gh_token: Optional[str]) -> None: |
|
|
|
"""Tags the release and generates a draft GitHub release""" |
|
|
|
"""Tags the release and generates a draft GitHub release""" |
|
|
|
|
|
|
|
|
|
|
|
# Make sure we're in a git repo. |
|
|
|
# Make sure we're in a git repo. |
|
|
@ -293,7 +298,12 @@ def tag(gh_token: Optional[str]): |
|
|
|
|
|
|
|
|
|
|
|
click.echo_via_pager(changes) |
|
|
|
click.echo_via_pager(changes) |
|
|
|
if click.confirm("Edit text?", default=False): |
|
|
|
if click.confirm("Edit text?", default=False): |
|
|
|
changes = click.edit(changes, require_save=False) |
|
|
|
edited_changes = click.edit(changes, require_save=False) |
|
|
|
|
|
|
|
# This assert is for mypy's benefit. click's docs are a little unclear, but |
|
|
|
|
|
|
|
# when `require_save=False`, not saving the temp file in the editor returns |
|
|
|
|
|
|
|
# the original string. |
|
|
|
|
|
|
|
assert edited_changes is not None |
|
|
|
|
|
|
|
changes = edited_changes |
|
|
|
|
|
|
|
|
|
|
|
repo.create_tag(tag_name, message=changes, sign=True) |
|
|
|
repo.create_tag(tag_name, message=changes, sign=True) |
|
|
|
|
|
|
|
|
|
|
@ -347,7 +357,7 @@ def tag(gh_token: Optional[str]): |
|
|
|
|
|
|
|
|
|
|
|
@cli.command() |
|
|
|
@cli.command() |
|
|
|
@click.option("--gh-token", envvar=["GH_TOKEN", "GITHUB_TOKEN"], required=True) |
|
|
|
@click.option("--gh-token", envvar=["GH_TOKEN", "GITHUB_TOKEN"], required=True) |
|
|
|
def publish(gh_token: str): |
|
|
|
def publish(gh_token: str) -> None: |
|
|
|
"""Publish release.""" |
|
|
|
"""Publish release.""" |
|
|
|
|
|
|
|
|
|
|
|
# Make sure we're in a git repo. |
|
|
|
# Make sure we're in a git repo. |
|
|
@ -390,7 +400,7 @@ def publish(gh_token: str): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@cli.command() |
|
|
|
@cli.command() |
|
|
|
def upload(): |
|
|
|
def upload() -> None: |
|
|
|
"""Upload release to pypi.""" |
|
|
|
"""Upload release to pypi.""" |
|
|
|
|
|
|
|
|
|
|
|
current_version = get_package_version() |
|
|
|
current_version = get_package_version() |
|
|
@ -418,7 +428,7 @@ def upload(): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@cli.command() |
|
|
|
@cli.command() |
|
|
|
def announce(): |
|
|
|
def announce() -> None: |
|
|
|
"""Generate markdown to announce the release.""" |
|
|
|
"""Generate markdown to announce the release.""" |
|
|
|
|
|
|
|
|
|
|
|
current_version = get_package_version() |
|
|
|
current_version = get_package_version() |
|
|
@ -461,18 +471,19 @@ def get_package_version() -> version.Version: |
|
|
|
|
|
|
|
|
|
|
|
def find_ref(repo: git.Repo, ref_name: str) -> Optional[git.HEAD]: |
|
|
|
def find_ref(repo: git.Repo, ref_name: str) -> Optional[git.HEAD]: |
|
|
|
"""Find the branch/ref, looking first locally then in the remote.""" |
|
|
|
"""Find the branch/ref, looking first locally then in the remote.""" |
|
|
|
if ref_name in repo.refs: |
|
|
|
if ref_name in repo.references: |
|
|
|
return repo.refs[ref_name] |
|
|
|
return repo.references[ref_name] |
|
|
|
elif ref_name in repo.remote().refs: |
|
|
|
elif ref_name in repo.remote().refs: |
|
|
|
return repo.remote().refs[ref_name] |
|
|
|
return repo.remote().refs[ref_name] |
|
|
|
else: |
|
|
|
else: |
|
|
|
return None |
|
|
|
return None |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def update_branch(repo: git.Repo): |
|
|
|
def update_branch(repo: git.Repo) -> None: |
|
|
|
"""Ensure branch is up to date if it has a remote""" |
|
|
|
"""Ensure branch is up to date if it has a remote""" |
|
|
|
if repo.active_branch.tracking_branch(): |
|
|
|
tracking_branch = repo.active_branch.tracking_branch() |
|
|
|
repo.git.merge(repo.active_branch.tracking_branch().name) |
|
|
|
if tracking_branch: |
|
|
|
|
|
|
|
repo.git.merge(tracking_branch.name) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_changes_for_version(wanted_version: version.Version) -> str: |
|
|
|
def get_changes_for_version(wanted_version: version.Version) -> str: |
|
|
@ -536,7 +547,9 @@ def get_changes_for_version(wanted_version: version.Version) -> str: |
|
|
|
return "\n".join(version_changelog) |
|
|
|
return "\n".join(version_changelog) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_and_write_changelog(current_version: version.Version, new_version: str): |
|
|
|
def generate_and_write_changelog( |
|
|
|
|
|
|
|
current_version: version.Version, new_version: str |
|
|
|
|
|
|
|
) -> None: |
|
|
|
# We do this by getting a draft so that we can edit it before writing to the |
|
|
|
# We do this by getting a draft so that we can edit it before writing to the |
|
|
|
# changelog. |
|
|
|
# changelog. |
|
|
|
result = run_until_successful( |
|
|
|
result = run_until_successful( |
|
|
@ -558,8 +571,8 @@ def generate_and_write_changelog(current_version: version.Version, new_version: |
|
|
|
f.write(existing_content) |
|
|
|
f.write(existing_content) |
|
|
|
|
|
|
|
|
|
|
|
# Remove all the news fragments |
|
|
|
# Remove all the news fragments |
|
|
|
for f in glob.iglob("changelog.d/*.*"): |
|
|
|
for filename in glob.iglob("changelog.d/*.*"): |
|
|
|
os.remove(f) |
|
|
|
os.remove(filename) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
|
if __name__ == "__main__": |
|
|
|