diff --git a/.github/workflows/make_release.yaml b/.github/workflows/make_release.yaml new file mode 100644 index 0000000000..9e24e1b9cc --- /dev/null +++ b/.github/workflows/make_release.yaml @@ -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 + + \ No newline at end of file diff --git a/extra/release.py b/extra/release.py index c3142268fb..8ad63cfe93 100755 --- a/extra/release.py +++ b/extra/release.py @@ -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 = [ @@ -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. """ @@ -105,134 +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)) + 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!\n" - + header += ( + "Changelog goes here! Please add your entry to the bottom of" + " one of the lists below!\n" + ) # Insert into the right place. 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. 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() @@ -260,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)