Skip to content

Commit

Permalink
Add CI workflows to streamline release process
Browse files Browse the repository at this point in the history
  • Loading branch information
LilSpazJoekp committed Feb 8, 2021
1 parent cf17939 commit 1f780b4
Show file tree
Hide file tree
Showing 6 changed files with 273 additions and 14 deletions.
47 changes: 47 additions & 0 deletions .github/workflows/prepare_release.yml
@@ -0,0 +1,47 @@
jobs:
prepare_release:
name: Prepare Release v${{ github.event.inputs.version }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
ssh-key: ${{ secrets.SSH_DEPLOY_KEY }}
- uses: actions/setup-python@v2
with:
python-version: 3.x
- name: Install dependencies
run: pip install packaging
- name: Prepare Git Variables
run: |
git config --global author.email ${{ github.actor }}@users.noreply.github.com
git config --global author.name ${{ github.actor }}
git config --global committer.email noreply@github.com
git config --global committer.name GitHub
- name: Set desired version
run: |
tools/set_version.py ${{ github.event.inputs.version }} > tmp_version
echo "version=$(cat tmp_version)" >> $GITHUB_ENV
- name: Commit desired version
run: git commit -am "Bump to v${{ env.version }}"
- name: Set development version
run: |
tools/set_version.py Unreleased > tmp_version
echo "dev_version=$(cat tmp_version)" >> $GITHUB_ENV
rm tmp_version
- name: Commit development version
run: git commit -am "Set development version v${{ env.dev_version }}"
- name: Create Pull Request
uses: peter-evans/create-pull-request@v3
with:
body:
branch: prepare_release_v${{ env.version }}
draft: false
title: Release v${{ env.version }}

name: Prepare Release
on:
workflow_dispatch:
inputs:
version:
description: 'The version to prepare for release'
required: true
24 changes: 10 additions & 14 deletions .github/workflows/python-publish.yml
@@ -1,28 +1,24 @@
name: Upload Python Package

on:
release:
types: [created]

jobs:
deploy:

release:
name: Build and release
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
- uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine
- name: Build and publish
env:
- env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
name: Build and release
run: |
python setup.py sdist bdist_wheel
twine upload dist/*
name: Upload Python Package
on:
release:
types: [published]
43 changes: 43 additions & 0 deletions .github/workflows/tag_release.yml
@@ -0,0 +1,43 @@
jobs:
release_tag:
if: "startsWith(github.event.head_commit.message, 'Merge pull request #') && contains(github.event.head_commit.message, ' from praw-dev/prepare_release_v')"
name: Tag Release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 3
ssh-key: ${{ secrets.SSH_DEPLOY_KEY }}
- uses: actions/setup-python@v2
with:
python-version: 3.x
- name: Install dependencies
run: pip install packaging docutils
- name: Extract Version
run: |
git checkout HEAD^2^
echo "commit=$(git rev-parse HEAD)" >> $GITHUB_ENV
git log --format=%B -n 1 | ./tools/bump_version.py > tmp_version
echo "version=$(cat tmp_version)" >> $GITHUB_ENV
cat tmp_version | python -c 'import sys; from packaging import version; print(int(version.Version(sys.stdin.readline()).is_prerelease))' > tmp_is_prerelease
echo "is_prerelease=$(cat tmp_is_prerelease)" >> $GITHUB_ENV
- name: Extract Change Log
run: |
echo ${{ env.version }} | ./tools/extract_log_entry.py > version_changelog
- env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
name: Create GitHub Release
uses: actions/create-release@v1
with:
body_path: version_changelog
commitish: ${{ env.commit }}
draft: true
prerelease: ${{ env.is_prerelease == '1' }}
release_name: v${{ env.version }}
tag_name: v${{ env.version }}
name: Tag Release
on:
push:
branches:
- master
- release_test
18 changes: 18 additions & 0 deletions tools/bump_version.py
@@ -0,0 +1,18 @@
#!/usr/bin/env python3
import sys

COMMIT_PREFIX = "Bump to v"


def main():
line = sys.stdin.readline()
if not line.startswith(COMMIT_PREFIX):
sys.stderr.write(
f"Commit message does not begin with `{COMMIT_PREFIX}`.\nMessage:\n\n{line}"
)
return 1
print(line[len(COMMIT_PREFIX) : -1])


if __name__ == "__main__":
sys.exit(main())
39 changes: 39 additions & 0 deletions tools/extract_log_entry.py
@@ -0,0 +1,39 @@
#!/usr/bin/env python3
import sys

import docutils.nodes
import docutils.parsers.rst
import docutils.utils


def get_entry_slice(doc):
current_version = sys.stdin.readline().strip()
start_line = None
end_line = None
for section in doc.children[0].children:
if start_line:
end_line = section.children[0].line - 2
break
header = section.children[0]
if current_version in header.rawsource:
start_line = header.line - 2
return slice(start_line, end_line)


def parse_rst(text: str) -> docutils.nodes.document:
parser = docutils.parsers.rst.Parser()
components = (docutils.parsers.rst.Parser,)
settings = docutils.frontend.OptionParser(
components=components
).get_default_values()
settings.report_level = 4
document = docutils.utils.new_document("<rst-doc>", settings=settings)
parser.parse(text, document)
return document


with open("CHANGES.rst") as f:
source = f.read()
document = parse_rst(source)

sys.stdout.write("\n".join(source.splitlines()[get_entry_slice(document)]))
116 changes: 116 additions & 0 deletions tools/set_version.py
@@ -0,0 +1,116 @@
#!/usr/bin/env python3
import re
import sys
from datetime import date

import packaging.version

CHANGELOG_HEADER = "Change Log\n==========\n\n"
UNRELEASED_HEADER = "Unreleased\n----------\n\n"


def add_unreleased_to_changelog():
with open("CHANGES.rst") as fp:
content = fp.read()

if not content.startswith(CHANGELOG_HEADER):
sys.stderr.write("Unexpected CHANGES.rst header\n")
return False
new_header = f"{CHANGELOG_HEADER}{UNRELEASED_HEADER}"
if content.startswith(new_header):
sys.stderr.write("CHANGES.rst already contains Unreleased header\n")
return False

with open("CHANGES.rst", "w") as fp:
fp.write(f"{new_header}{content[len(CHANGELOG_HEADER):]}")
return True


def handle_unreleased():
return add_unreleased_to_changelog() and increment_development_version()


def handle_version(version):
version = valid_version(version)
if not version:
return False
return update_changelog(version) and update_package(version)


def increment_development_version():
with open("asyncpraw/const.py") as fp:
version = re.search('__version__ = "([^"]+)"', fp.read()).group(1)

parsed_version = valid_version(version)
if not parsed_version:
return False

if parsed_version.is_devrelease:
pre = "".join(str(x) for x in parsed_version.pre) if parsed_version.pre else ""
new_version = f"{parsed_version.base_version}{pre}.dev{parsed_version.dev + 1}"
elif parsed_version.is_prerelease:
new_version = f"{parsed_version}.dev0"
else:
assert parsed_version.base_version == version
new_version = f"{parsed_version.major}.{parsed_version.minor}.{parsed_version.micro + 1}.dev0"

assert valid_version(new_version)
return update_package(new_version)


def main():
if len(sys.argv) != 2:
sys.stderr.write(f"Usage: {sys.argv[0]} VERSION\n")
return 1
if sys.argv[1] == "Unreleased":
return not handle_unreleased()
return not handle_version(sys.argv[1])


def update_changelog(version):
with open("CHANGES.rst") as fp:
content = fp.read()

expected_header = f"{CHANGELOG_HEADER}{UNRELEASED_HEADER}"
if not content.startswith(expected_header):
sys.stderr.write("CHANGES.rst does not contain Unreleased header.\n")
return False

date_string = date.today().strftime("%Y/%m/%d")
version_line = f"{version} ({date_string})\n"
version_header = f"{version_line}{'-' * len(version_line[:-1])}\n\n"

with open("CHANGES.rst", "w") as fp:
fp.write(f"{CHANGELOG_HEADER}{version_header}{content[len(expected_header):]}")
return True


def update_package(version):
with open("asyncpraw/const.py") as fp:
content = fp.read()

updated = re.sub('__version__ = "([^"]+)"', f'__version__ = "{version}"', content)
if content == updated:
sys.stderr.write("Package version string not changed\n")
return False

with open("asyncpraw/const.py", "w") as fp:
fp.write(updated)

print(version)
return True


def valid_version(version):
parsed_version = packaging.version.parse(version)
if isinstance(parsed_version, packaging.version.LegacyVersion):
sys.stderr.write(f"Invalid PEP 440 version: {version}\n")
return False
if parsed_version.local or parsed_version.is_postrelease or parsed_version.epoch:
sys.stderr.write("epoch, local postrelease version parts are not supported")
return False
return parsed_version


if __name__ == "__main__":
sys.exit(main())

0 comments on commit 1f780b4

Please sign in to comment.