mirror of
https://github.com/beetbox/beets.git
synced 2025-12-06 08:39:17 +01:00
This is 'the big one', which touches every file so that it all conforms to the given standard.
367 lines
9.6 KiB
Python
Executable file
367 lines
9.6 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
|
|
"""A utility script for automating the beets release process.
|
|
"""
|
|
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
|
|
|
|
|
|
# Locations (filenames and patterns) of the version number.
|
|
VERSION_LOCS = [
|
|
(
|
|
os.path.join(BASE, "beets", "__init__.py"),
|
|
[
|
|
(
|
|
r'__version__\s*=\s*[\'"]([0-9\.]+)[\'"]',
|
|
"__version__ = '{version}'",
|
|
)
|
|
],
|
|
),
|
|
(
|
|
os.path.join(BASE, "docs", "conf.py"),
|
|
[
|
|
(
|
|
r'version\s*=\s*[\'"]([0-9\.]+)[\'"]',
|
|
"version = '{minor}'",
|
|
),
|
|
(
|
|
r'release\s*=\s*[\'"]([0-9\.]+)[\'"]',
|
|
"release = '{version}'",
|
|
),
|
|
],
|
|
),
|
|
(
|
|
os.path.join(BASE, "setup.py"),
|
|
[
|
|
(
|
|
r'\s*version\s*=\s*[\'"]([0-9\.]+)[\'"]',
|
|
" version='{version}',",
|
|
)
|
|
],
|
|
),
|
|
]
|
|
|
|
GITHUB_USER = "beetbox"
|
|
GITHUB_REPO = "beets"
|
|
|
|
|
|
def bump_version(version):
|
|
"""Update the version number in setup.py, docs config, changelog,
|
|
and root module.
|
|
"""
|
|
version_parts = [int(p) for p in version.split(".")]
|
|
assert len(version_parts) == 3, "invalid version number"
|
|
minor = "{}.{}".format(*version_parts)
|
|
major = "{}".format(*version_parts)
|
|
|
|
# Replace the version each place where it lives.
|
|
for filename, locations in VERSION_LOCS:
|
|
# Read and transform the file.
|
|
out_lines = []
|
|
with open(filename) as f:
|
|
found = False
|
|
for line in f:
|
|
for pattern, template in locations:
|
|
match = re.match(pattern, line)
|
|
if match:
|
|
# Check that this version is actually newer.
|
|
old_version = match.group(1)
|
|
old_parts = [int(p) for p in old_version.split(".")]
|
|
assert (
|
|
version_parts > old_parts
|
|
), "version must be newer than {}".format(old_version)
|
|
|
|
# Insert the new version.
|
|
out_lines.append(
|
|
template.format(
|
|
version=version,
|
|
major=major,
|
|
minor=minor,
|
|
)
|
|
+ "\n"
|
|
)
|
|
|
|
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))
|
|
|
|
# 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!\n"
|
|
|
|
# Insert into the right place.
|
|
with open(CHANGELOG) as f:
|
|
contents = f.read()
|
|
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()
|
|
@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).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():
|
|
"""Enter today's date as the release date in the changelog."""
|
|
dt = datetime.datetime.now()
|
|
stamp = "({} {}, {})".format(dt.strftime("%B"), dt.day, dt.year)
|
|
marker = "(in development)"
|
|
|
|
lines = []
|
|
underline_length = None
|
|
with open(CHANGELOG) as f:
|
|
for line in f:
|
|
if marker in line:
|
|
# The header line.
|
|
line = line.replace(marker, stamp)
|
|
lines.append(line)
|
|
underline_length = len(line.strip())
|
|
elif underline_length:
|
|
# This is the line after the header. Rewrite the dashes.
|
|
lines.append("-" * underline_length + "\n")
|
|
underline_length = None
|
|
else:
|
|
lines.append(line)
|
|
|
|
with open(CHANGELOG, "w") as f:
|
|
for line in lines:
|
|
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)
|
|
|
|
# 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,
|
|
]
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
release()
|