Fix script for changelog

This commit is contained in:
Serene-Arc 2023-10-27 17:58:47 +10:00
parent 3800593046
commit 8c898ce524

View file

@ -2,31 +2,15 @@
"""A utility script for automating the beets release process. """A utility script for automating the beets release process.
""" """
import argparse
import datetime import datetime
import os import os
import re import re
import subprocess
from contextlib import contextmanager
import click
BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CHANGELOG = os.path.join(BASE, "docs", "changelog.rst") CHANGELOG = os.path.join(BASE, "docs", "changelog.rst")
parser = argparse.ArgumentParser()
parser.add_argument("version", type=str)
@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
# Locations (filenames and patterns) of the version number. # Locations (filenames and patterns) of the version number.
VERSION_LOCS = [ VERSION_LOCS = [
@ -67,7 +51,7 @@ GITHUB_USER = "beetbox"
GITHUB_REPO = "beets" GITHUB_REPO = "beets"
def bump_version(version): def bump_version(version: str):
"""Update the version number in setup.py, docs config, changelog, """Update the version number in setup.py, docs config, changelog,
and root module. and root module.
""" """
@ -105,129 +89,45 @@ def bump_version(version):
found = True found = True
break break
else: else:
# Normal line. # Normal line.
out_lines.append(line) out_lines.append(line)
if not found: if not found:
print(f"No pattern found in {filename}") print(f"No pattern found in {filename}")
# Write the file back. # Write the file back.
with open(filename, "w") as f: with open(filename, "w") as f:
f.write("".join(out_lines)) f.write("".join(out_lines))
update_changelog(version)
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"
)
# Insert into the right place. # Insert into the right place.
with open(CHANGELOG) as f: with open(CHANGELOG) as f:
contents = f.read() contents = f.readlines()
location = contents.find("\n\n") # First blank line.
contents = contents[:location] + contents[location:]
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)
location = contents.find("\n\n") # First blank line.
contents = contents[:location] + header + contents[location:]
# Write back. # Write back.
with open(CHANGELOG, "w") as f: with open(CHANGELOG, "w") as f:
f.write(contents) f.write(contents)
@release.command()
@click.argument("version")
def bump(version):
"""Bump the version number."""
bump_version(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,
)
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."""
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
@release.command()
def version():
"""Display the current version."""
print(get_version())
@release.command()
def datestamp(): def datestamp():
"""Enter today's date as the release date in the changelog.""" """Enter today's date as the release date in the changelog."""
dt = datetime.datetime.now() dt = datetime.datetime.now()
@ -255,108 +155,12 @@ def datestamp():
f.write(line) f.write(line)
@release.command() def prep(args: argparse.Namespace):
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)
# Version number bump. # Version number bump.
# FIXME It should be possible to specify this as an argument. datestamp()
version_parts = [int(n) for n in cur_version.split(".")] bump_version(args.version)
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,
]
)
if __name__ == "__main__": if __name__ == "__main__":
release() args = parser.parse_args()
prep(args)