Skip to content

Commit

Permalink
Fix script for changelog
Browse files Browse the repository at this point in the history
  • Loading branch information
Serene-Arc committed Oct 27, 2023
1 parent 3800593 commit 8c898ce
Showing 1 changed file with 31 additions and 227 deletions.
258 changes: 31 additions & 227 deletions extra/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,15 @@

"""A utility script for automating the beets release process.
"""
import argparse
import datetime
import os
import re
import subprocess
from contextlib import contextmanager

import click

BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CHANGELOG = os.path.join(BASE, "docs", "changelog.rst")


@contextmanager
def chdir(d):
"""A context manager that temporary changes the working directory."""
olddir = os.getcwd()
os.chdir(d)
yield
os.chdir(olddir)


@click.group()
def release():
pass

parser = argparse.ArgumentParser()
parser.add_argument("version", type=str)

# Locations (filenames and patterns) of the version number.
VERSION_LOCS = [
Expand Down Expand Up @@ -67,7 +51,7 @@ def release():
GITHUB_REPO = "beets"


def bump_version(version):
def bump_version(version: str):
"""Update the version number in setup.py, docs config, changelog,
and root module.
"""
Expand Down Expand Up @@ -105,129 +89,45 @@ def bump_version(version):

found = True
break

else:
# Normal line.
out_lines.append(line)

if not found:
print(f"No pattern found in {filename}")

# Write the file back.
with open(filename, "w") as f:
f.write("".join(out_lines))

# Insert into the right place.
with open(CHANGELOG) as f:
contents = f.read()
location = contents.find("\n\n") # First blank line.
contents = contents[:location] + contents[location:]

# Write back.
with open(CHANGELOG, "w") as f:
f.write(contents)


@release.command()
@click.argument("version")
def bump(version):
"""Bump the version number."""
bump_version(version)
update_changelog(version)


def get_latest_changelog():
"""Extract the first section of the changelog."""
started = False
lines = []
with open(CHANGELOG) as f:
for line in f:
if re.match(r"^--+$", line.strip()):
# Section boundary. Start or end.
if started:
# Remove last line, which is the header of the next
# section.
del lines[-1]
break
else:
started = True

elif started:
lines.append(line)
return "".join(lines[2:]).strip()


def rst2md(text):
"""Use Pandoc to convert text from ReST to Markdown."""
pandoc = subprocess.Popen(
["pandoc", "--from=rst", "--to=markdown", "--wrap=none"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
def update_changelog(version: str):
# Generate bits to insert into changelog.
header_line = f"{version} (in development)"
header = "\n\n" + header_line + "\n" + "-" * len(header_line) + "\n\n"
header += (
"Changelog goes here! Please add your entry to the bottom of"
" one of the lists below!\n"
)
stdout, _ = pandoc.communicate(text.encode("utf-8"))
md = stdout.decode("utf-8").strip()

# Fix up odd spacing in lists.
return re.sub(r"^- ", "- ", md, flags=re.M)


def changelog_as_markdown():
"""Get the latest changelog entry as hacked up Markdown."""
rst = get_latest_changelog()

# Replace plugin links with plugin names.
rst = re.sub(r":doc:`/plugins/(\w+)`", r"``\1``", rst)

# References with text.
rst = re.sub(r":ref:`([^<]+)(<[^>]+>)`", r"\1", rst)

# Other backslashes with verbatim ranges.
rst = re.sub(r"(\s)`([^`]+)`([^_])", r"\1``\2``\3", rst)

# Command links with command names.
rst = re.sub(r":ref:`(\w+)-cmd`", r"``\1``", rst)

# Bug numbers.
rst = re.sub(r":bug:`(\d+)`", r"#\1", rst)

# Users.
rst = re.sub(r":user:`(\w+)`", r"@\1", rst)

# Convert with Pandoc.
md = rst2md(rst)

# Restore escaped issue numbers.
md = re.sub(r"\\#(\d+)\b", r"#\1", md)

return md


@release.command()
def changelog():
"""Get the most recent version's changelog as Markdown."""
print(changelog_as_markdown())


def get_version(index=0):
"""Read the current version from the changelog."""
# Insert into the right place.
with open(CHANGELOG) as f:
cur_index = 0
for line in f:
match = re.search(r"^\d+\.\d+\.\d+", line)
if match:
if cur_index == index:
return match.group(0)
else:
cur_index += 1
contents = f.readlines()

contents = [
line
for line in contents
if not re.match(r"Changelog goes here!.*", line)
]
contents = "".join(contents)
contents = re.sub("\n{3,}", "\n\n", contents)

@release.command()
def version():
"""Display the current version."""
print(get_version())
location = contents.find("\n\n") # First blank line.
contents = contents[:location] + header + contents[location:]
# Write back.
with open(CHANGELOG, "w") as f:
f.write(contents)


@release.command()
def datestamp():
"""Enter today's date as the release date in the changelog."""
dt = datetime.datetime.now()
Expand Down Expand Up @@ -255,108 +155,12 @@ def datestamp():
f.write(line)


@release.command()
def prep():
"""Run all steps to prepare a release.
- Tag the commit.
- Build the sdist package.
- Generate the Markdown changelog to ``changelog.md``.
- Bump the version number to the next version.
"""
cur_version = get_version()

# Tag.
subprocess.check_call(["git", "tag", f"v{cur_version}"])

# Build.
with chdir(BASE):
subprocess.check_call(["python", "setup.py", "sdist"])

# Generate Markdown changelog.
cl = changelog_as_markdown()
with open(os.path.join(BASE, "changelog.md"), "w") as f:
f.write(cl)

def prep(args: argparse.Namespace):
# Version number bump.
# FIXME It should be possible to specify this as an argument.
version_parts = [int(n) for n in cur_version.split(".")]
version_parts[-1] += 1
next_version = ".".join(map(str, version_parts))
bump_version(next_version)


@release.command()
def publish():
"""Unleash a release unto the world.
- Push the tag to GitHub.
- Upload to PyPI.
"""
version = get_version(1)

# Push to GitHub.
with chdir(BASE):
subprocess.check_call(["git", "push"])
subprocess.check_call(["git", "push", "--tags"])

# Upload to PyPI.
path = os.path.join(BASE, "dist", f"beets-{version}.tar.gz")
subprocess.check_call(["twine", "upload", path])


@release.command()
def ghrelease():
"""Create a GitHub release using the `github-release` command-line
tool.
Reads the changelog to upload from `changelog.md`. Uploads the
tarball from the `dist` directory.
"""
version = get_version(1)
tag = "v" + version

# Load the changelog.
with open(os.path.join(BASE, "changelog.md")) as f:
cl_md = f.read()

# Create the release.
subprocess.check_call(
[
"github-release",
"release",
"-u",
GITHUB_USER,
"-r",
GITHUB_REPO,
"--tag",
tag,
"--name",
f"{GITHUB_REPO} {version}",
"--description",
cl_md,
]
)

# Attach the release tarball.
tarball = os.path.join(BASE, "dist", f"beets-{version}.tar.gz")
subprocess.check_call(
[
"github-release",
"upload",
"-u",
GITHUB_USER,
"-r",
GITHUB_REPO,
"--tag",
tag,
"--name",
os.path.basename(tarball),
"--file",
tarball,
]
)
datestamp()
bump_version(args.version)


if __name__ == "__main__":
release()
args = parser.parse_args()
prep(args)

0 comments on commit 8c898ce

Please sign in to comment.