From e630aeaee23871089bc99617d25b29ca9bda951e Mon Sep 17 00:00:00 2001 From: Norman Rzepka Date: Wed, 8 Dec 2021 16:55:43 +0100 Subject: [PATCH] GH Action for creating releases (#503) * adds tools for changelog and release management * use github token * github_token is used by default, duh * also check for already-existing higher versions * workflow * fixes * pr feedback * Update make_changelog.py Co-authored-by: Philipp Otto Co-authored-by: Philipp Otto --- .github/workflows/release.yml | 47 ++++++++++++++++++++ check_version.py | 20 +++++++++ make_changelog.py | 84 +++++++++++++++++++++++++++++++++++ make_release.sh | 30 +++++++++++++ 4 files changed, 181 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100644 check_version.py create mode 100644 make_changelog.py create mode 100755 make_release.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..21297feec --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,47 @@ +name: Automatic release + +on: + workflow_dispatch: + inputs: + version: + description: "Version of the new release (e.g. `0.0.1`)" + required: true + +jobs: + release: + runs-on: ubuntu-latest + env: + VERSION: ${{ github.event.inputs.version }} + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + ref: auto-changelogs + + - uses: actions/setup-python@v1 + with: + python-version: "3.8" + architecture: 'x64' + + - name: Setup git config + run: | + git config user.name "Automatic release" + git config user.email "<>" + + - name: Check whether tag already exists + run: | + if git show-ref --tags "v${VERSION}" --quiet; then + echo "Version $VERSION already exists. Stopping." + exit 1 + fi + + - name: Make and push release + run: | + ./make_release.sh ${VERSION} + git add */Changelog.md + + git commit -m "Release for v${VERSION}" + git tag v${VERSION} + + git push origin master + git push --tags \ No newline at end of file diff --git a/check_version.py b/check_version.py new file mode 100644 index 000000000..85469ae24 --- /dev/null +++ b/check_version.py @@ -0,0 +1,20 @@ +from subprocess import run +from re import match +import sys + +this_version = tuple(int(a) for a in sys.argv[1].split(".")) + +max_git_tag = max( + [ + tuple(int(a) for a in line[1:].split(".")) + for line in run(["git", "tag"], capture_output=True) + .stdout.decode("utf-8") + .split("\n") + if match("^v\d+\.\d+.\d+$", line) + ] +) + +if this_version > max_git_tag: + sys.exit(0) +else: + sys.exit(1) diff --git a/make_changelog.py b/make_changelog.py new file mode 100644 index 000000000..b48f319ea --- /dev/null +++ b/make_changelog.py @@ -0,0 +1,84 @@ +import datetime +import re +import sys + +# Read existing changelog +with open("Changelog.md") as f: + changelog_lines = list(s.rstrip() for s in f) + +# Determine new version +this_version = sys.argv[1] +today = datetime.date.today() +today_str = f"{today.strftime('%Y')}-{today.strftime('%m')}-{today.strftime('%d')}" + +# Determine last version +matches = re.finditer(r"^## \[v?(.*)\].*$", "\n".join(changelog_lines), re.MULTILINE) +last_version = next(matches)[1] + +# Stop the script if new version is already the latest +if last_version == this_version: + print(f"Version {this_version} is already up-to-date.") + sys.exit(0) + +# Find line with "## Unreleased" heading +unreleased_idx = next( + i for i, line in enumerate(changelog_lines) if line.startswith("## Unreleased") +) + +# Find line with the last release (i.e. "## x.y.z" heading) +last_release_idx = next( + i + for i, line in enumerate(changelog_lines) + if line.startswith("## ") and i > unreleased_idx +) + +# Clean up unreleased notes (i.e. remove empty sections) +released_notes = "\n".join(changelog_lines[(unreleased_idx + 2) : last_release_idx]) + +release_section_fragments = re.split("\n### (.*)\n", released_notes, re.MULTILINE) +release_notes_intro = release_section_fragments[0] +release_sections = list( + zip(release_section_fragments[1::2], release_section_fragments[2::2]) +) +nonempty_release_sections = [ + (section, content) for section, content in release_sections if content.strip() != "" +] + +cleaned_release_notes = ( + "\n".join( + [release_notes_intro] + + [ + f"### {section}\n{content}" + for section, content in nonempty_release_sections + ] + ) +).split("\n") + +# Update changelog +lines_to_insert = [ + "## Unreleased", + f"[Commits](https://github.com/scalableminds/webknossos-libs/compare/v{this_version}...HEAD)", + "", + "### Breaking Changes", + "", + "### Added", + "", + "### Changed", + "", + "### Fixed", + "", + "", + f"## [{this_version}](https://github.com/scalableminds/webknossos-libs/releases/tag/v{this_version}) - {today_str}", + f"[Commits](https://github.com/scalableminds/webknossos-libs/compare/v{last_version}...v{this_version})", +] +changelog_lines = ( + changelog_lines[:unreleased_idx] + + lines_to_insert + + cleaned_release_notes + + [""] + + changelog_lines[(last_release_idx):] + + [""] +) + +with open("Changelog.md", "wt") as f: + f.write("\n".join(changelog_lines)) diff --git a/make_release.sh b/make_release.sh new file mode 100755 index 000000000..b0eda6eb3 --- /dev/null +++ b/make_release.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -eEuo pipefail +set +x + +if [[ $# -eq 0 ]] ; then + echo "Please supply a 'version' as argument." + exit 1 +fi + +PKG_VERSION="$1" + +if ! python check_version.py ${PKG_VERSION}; then + echo "A higher version is already present." + exit 1 +fi + +for PKG in */pyproject.toml; do + PKG="$(dirname "$PKG")" + if [[ "$PKG" == "docs" ]]; then + echo Skipping "$PKG" + continue + fi + echo "Creating release for $PKG" + + pushd "$PKG" > /dev/null + + python ../make_changelog.py "$PKG_VERSION" + + popd > /dev/null +done