mirror of
https://github.com/beetbox/beets.git
synced 2025-12-06 08:39:17 +01:00
Add a workflow for easily creating releases (#4952)
I humbly present a solution our lack of releases: a workflow that can be triggered to automatically create one. This workflow builds the project, creates a GitHub release, and publishes beets to PyPi, for a one-stop solution. @sampsyo this would make it much easier to create releases, as it requires only one little interaction: going to the actions tab and entering a version number. Once that's done, the workflow should take care of the rest. I have only tested the `build` job so far, since I can't do anything about the pypi or do a release just to test, but the code is lifted from other similar actions and should work fine. It also requires one piece of setup. This is that PyPi must be set up with a [trusted publisher](https://docs.pypi.org/trusted-publishers/) to receive the new package. Once that's done, the process should go off automatically.
This commit is contained in:
commit
b88c09720c
2 changed files with 118 additions and 228 deletions
91
.github/workflows/make_release.yaml
vendored
Normal file
91
.github/workflows/make_release.yaml
vendored
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
name: Make a Beets Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: 'Version of the new release'
|
||||||
|
required: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
increment_version:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install pandoc
|
||||||
|
run: sudo apt update && sudo apt install pandoc -y
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: "3.9"
|
||||||
|
- name: Run version script
|
||||||
|
id: script
|
||||||
|
run: |
|
||||||
|
python extra/release.py "${{ inputs.version }}"
|
||||||
|
- uses: EndBug/add-and-commit@v9
|
||||||
|
name: Commit the changes
|
||||||
|
with:
|
||||||
|
message: 'Increment version numbers to ${{ inputs.version }}'
|
||||||
|
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: increment_version
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: "3.9"
|
||||||
|
- run: pip install build wheel sphinx
|
||||||
|
- name: Build a binary wheel and a source tarball
|
||||||
|
env:
|
||||||
|
TZ: UTC
|
||||||
|
run: python3 -m build
|
||||||
|
- name: Store the distribution packages
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: python-package-distributions
|
||||||
|
path: dist/
|
||||||
|
|
||||||
|
make_github_release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Bump version and push tag
|
||||||
|
id: tag_version
|
||||||
|
uses: mathieudutour/github-tag-action@v6.1
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
custom_tag: ${{ inputs.version }}
|
||||||
|
- name: Download all the dists
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: python-package-distributions
|
||||||
|
path: dist/
|
||||||
|
- name: Create a GitHub release
|
||||||
|
uses: ncipollo/release-action@v1
|
||||||
|
with:
|
||||||
|
tag: ${{ steps.tag_version.outputs.new_tag }}
|
||||||
|
name: Release ${{ steps.tag_version.outputs.new_tag }}
|
||||||
|
body: "Check [here](https://beets.readthedocs.io/en/stable/changelog.html) for the latest changes."
|
||||||
|
artifacts: dist/*
|
||||||
|
|
||||||
|
publish_to_pypi:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
environment:
|
||||||
|
name: pypi
|
||||||
|
url: https://pypi.org/p/beets
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
steps:
|
||||||
|
- name: Download all the dists
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: python-package-distributions
|
||||||
|
path: dist/
|
||||||
|
- name: Publish distribution 📦 to PyPI
|
||||||
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
|
|
||||||
|
|
||||||
255
extra/release.py
255
extra/release.py
|
|
@ -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,134 +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.
|
# Generate bits to insert into changelog.
|
||||||
header_line = f"{version} (in development)"
|
header_line = f"{version} (in development)"
|
||||||
header = "\n\n" + header_line + "\n" + "-" * len(header_line) + "\n\n"
|
header = "\n\n" + header_line + "\n" + "-" * len(header_line) + "\n\n"
|
||||||
header += "Changelog goes here!\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()
|
||||||
|
|
||||||
|
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.
|
location = contents.find("\n\n") # First blank line.
|
||||||
contents = contents[:location] + header + contents[location:]
|
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).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()
|
||||||
|
|
@ -260,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)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue