Skip to content

Commit

Permalink
Automate the existing manual release process using GitHub Actions
Browse files Browse the repository at this point in the history
  • Loading branch information
pronovic committed Dec 30, 2022
1 parent 4e8e8a9 commit cb95d8f
Show file tree
Hide file tree
Showing 11 changed files with 95 additions and 144 deletions.
21 changes: 18 additions & 3 deletions .github/workflows/test-suite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ on:
push:
branches:
- master
tags:
- "v*"
pull_request:
branches:
- master
Expand All @@ -15,26 +17,39 @@ concurrency:
cancel-in-progress: true
jobs:
linux-build-and-test:
uses: pronovic/gha-shared-workflows/.github/workflows/poetry-build-and-test.yml@v2
name: "Linux"
uses: pronovic/gha-shared-workflows/.github/workflows/poetry-build-and-test.yml@v3
secrets: inherit
with:
matrix-os-version: "[ 'ubuntu-latest' ]"
matrix-python-version: "[ '3.7', '3.8', '3.9', '3.10', '3.11' ]" # run Linux tests on all supported Python versions
poetry-version: "1.3.1"
enable-coveralls: true # only report to coveralls.io for tests that run on Linux
macos-build-and-test:
uses: pronovic/gha-shared-workflows/.github/workflows/poetry-build-and-test.yml@v2
name: "MacOS"
uses: pronovic/gha-shared-workflows/.github/workflows/poetry-build-and-test.yml@v3
secrets: inherit
with:
matrix-os-version: "[ 'macos-latest' ]"
matrix-python-version: "[ '3.11' ]" # only run MacOS tests on latest Python
poetry-version: "1.3.1"
enable-coveralls: false
windows-build-and-test:
uses: pronovic/gha-shared-workflows/.github/workflows/poetry-build-and-test.yml@v2
name: "Windows"
uses: pronovic/gha-shared-workflows/.github/workflows/poetry-build-and-test.yml@v3
secrets: inherit
with:
matrix-os-version: "[ 'windows-latest' ]"
matrix-python-version: "[ '3.11' ]" # only run Windows tests on latest Python
poetry-version: "1.3.1"
enable-coveralls: false
release:
name: "Release"
if: github.ref_type == 'tag'
uses: pronovic/gha-shared-workflows/.github/workflows/poetry-release.yml@v3
needs: [ linux-build-and-test, macos-build-and-test, windows-build-and-test ]
secrets: inherit
with:
python-version: "3.8"
poetry-version: "1.3.1"
publish-pypi: true
13 changes: 13 additions & 0 deletions .run/commands/bumpchangelog.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# vim: set ft=bash ts=3 sw=3 expandtab:
# Bump the version in the changelog, preparing for a new development cycle

command_bumpchangelog() {
mv Changelog Changelog.$$
echo "Version $(poetry version patch --short) unreleased" > Changelog
echo "" >> Changelog
echo $'\t* ' >> Changelog
echo "" >> Changelog
cat Changelog.$$ >> Changelog
rm -f Changelog.$$
}

28 changes: 0 additions & 28 deletions .run/commands/publishpypi.sh

This file was deleted.

33 changes: 11 additions & 22 deletions .run/commands/tagrelease.sh
Original file line number Diff line number Diff line change
@@ -1,29 +1,19 @@
# vim: set ft=bash ts=3 sw=3 expandtab:
# Update the changelog and tag a specific version of the code
# Update the changelog, tag a specific version of the code, and push the changes

command_tagrelease() {
local VERSION EARLIEST_YEAR LATEST_YEAR DEFAULT_BRANCH CURRENT_BRANCH COPYRIGHT DATE TAG FILES MESSAGE
local VERSION DATE TAG FILES MESSAGE

if [ $# != 1 ]; then
echo "tagrelease <version>"
exit 1
fi

VERSION=$(echo "$1" | sed 's/^v//') # so you can use "0.1.5 or "v0.1.5"
EARLIEST_YEAR=$(git log --pretty="%ci" $(git rev-list --max-parents=0 HEAD) | sed 's/-.*$//g')
LATEST_YEAR=$(git log -1 --pretty="%ci" | sed 's/-.*$//g')
DEFAULT_BRANCH=$(git config --get init.defaultBranch) # works on git > 2.28.0 from 2020
CURRENT_BRANCH=$(git branch -a | grep '^\*' | sed 's/^\* //')
DATE=$(date +'%d %b %Y')
TAG="v$VERSION" # follow PEP 440 naming convention
FILES="NOTICE pyproject.toml Changelog"
MESSAGE="Release v$VERSION to PyPI"

if [ "$EARLIEST_YEAR" == "$LATEST_YEAR" ]; then
COPYRIGHT="${EARLIEST_YEAR}"
else
COPYRIGHT="${EARLIEST_YEAR}-${LATEST_YEAR}"
fi
FILES="NOTICE Changelog"
MESSAGE="Release v$VERSION"

if [ "$CURRENT_BRANCH" != "$DEFAULT_BRANCH" ]; then
echo "*** You are not on $DEFAULT_BRANCH; you cannot release from this branch"
Expand All @@ -42,13 +32,6 @@ command_tagrelease() {
exit 1
fi

poetry version $VERSION
if [ $? != 0 ]; then
echo "*** Failed to update version"
exit 1
fi

run_command dos2unix pyproject.toml
run_command sedreplace "s|^Version $VERSION[[:blank:]][[:blank:]]*unreleased|Version $VERSION $DATE|g" Changelog
run_command sedreplace "s|(^ *Copyright \(c\) *)([0-9,-]+)( *.*$)|\1$COPYRIGHT\3|" NOTICE

Expand All @@ -66,8 +49,14 @@ command_tagrelease() {
exit 1
fi

git push --follow-tags
if [ $? != 0 ]; then
echo "*** Push step failed"
exit 1
fi

echo ""
echo "*** Version v$VERSION has been released and committed; you may publish now"
echo "*** Version v$VERSION has been tagged"
echo ""
}

12 changes: 12 additions & 0 deletions .run/tasks/dch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# vim: set ft=bash sw=3 ts=3 expandtab:

help_dch() {
# No help - hidden utility feature, sort of equivalent to the Debian 'dch' command
echo -n ""
}

task_dch() {
run_command bumpchangelog
vim "+3" "+startinsert!" Changelog
}

10 changes: 0 additions & 10 deletions .run/tasks/publish.sh

This file was deleted.

2 changes: 1 addition & 1 deletion .run/tasks/release.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# vim: set ft=bash sw=3 ts=3 expandtab:

help_release() {
echo "- run release: Release a specific version and tag the code"
echo "- run release: Tag and release the code, triggering GHA to publish artifacts"
}

task_release() {
Expand Down
16 changes: 16 additions & 0 deletions .run/util.sh
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,25 @@ task_help() {

# Setup the runtime environment
setup_environment() {
local EARLIEST_YEAR LATEST_YEAR

DOTRUN_DIR="$REPO_DIR/.run"

WORKING_DIR=$(mktemp -d)
trap "rm -rf '$WORKING_DIR'" EXIT SIGINT SIGTERM

DEFAULT_BRANCH=$(git config --get init.defaultBranch) # works on git > 2.28.0 from 2020
CURRENT_BRANCH=$(git branch -a | grep '^\*' | sed 's/^\* //')

# Use $COPYRIGHT_START to override the earliest year found, in case git doesn't contain all history
EARLIEST_YEAR=${COPYRIGHT_START:-$(git log --pretty="%ci" $(git rev-list --max-parents=0 HEAD) | sed 's/-.*$//g')}
LATEST_YEAR=$(git log -1 --pretty="%ci" | sed 's/-.*$//g')

if [ "$EARLIEST_YEAR" == "$LATEST_YEAR" ]; then
COPYRIGHT="${EARLIEST_YEAR}"
else
COPYRIGHT="${EARLIEST_YEAR}-${LATEST_YEAR}"
fi
}

# Add addendum information to the end of the help output
Expand Down
1 change: 1 addition & 0 deletions Changelog
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ Version 0.1.17 unreleased

* Convert to latest readthedocs.io standard.
* Add NOTICE file to sdist, alongside existing LICENSE file.
* Automate the existing manual release process using GitHub Actions.
* Adjust GHA build to run MacOS and Windows tests only on latest Python.

Version 0.1.16 24 Oct 2022
Expand Down
89 changes: 12 additions & 77 deletions DEVELOPER.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,7 @@ Additional tasks:
- run docs: Build the Sphinx documentation for readthedocs.io
- run docs -o: Build the Sphinx documentation and open in a browser
- run publish: Publish the current code to PyPI and push to GitHub
- run release: Release a specific version and tag the code
- run release: Tag and release the code, triggering GHA to publish artifacts
```

## Integration with PyCharm
Expand Down Expand Up @@ -359,18 +358,16 @@ change the path for `bash.exe`.
### Documentation

Documentation at [Read the Docs](https://uci-parse.readthedocs.io/en/stable/)
is generated via a GitHub hook each time code is pushed to master. So, there
is no formal release process for the documentation.
is generated via a GitHub hook. So, there is no formal release process for the
documentation.

### Code

Code is released to [PyPI](https://pypi.org/project/uciparse/). There is a
partially-automated process to publish a new release.

> _Note:_ In order to publish code, you must must have push permissions to the
> GitHub repo and be a collaborator on the PyPI project. Before running this
> process for the first time, you must set up a PyPI API token and configure
> Poetry to use it. (See notes below.)
> GitHub repo.
Ensure that you are on the `master` branch. Releases must always be done from
`master`.
Expand All @@ -382,79 +379,17 @@ will be published. The top line must show your version as unreleased:
Version 0.1.29 unreleased
```

Run the release step:
Run the release command:

```
./run release 0.1.29
```

This updates `pyproject.toml` and the `Changelog` to reflect the released
version, then commits those changes and tags the code. Nothing has been pushed
or published yet, so you can always remove the tag (i.e. `git tag -d v0.1.29`)
and revert your commit (`git reset HEAD~1`) if you made a mistake.
This command updates `NOTICE` and `Changelog` to reflect the release version
and release date, commits those changes, tags the code, and pushes to GitHub.
The new tag triggers a GitHub Actions build that runs the test suite, generates
the artifacts, publishes to PyPI, and finally creates a release from the tag.

Finally, publish the release:

```
./run publish
```

This builds the deployment artifacts, publishes the artifacts to PyPI, and
pushes the repo to GitHub. The code will be available on PyPI for others to
use after a little while.

### Configuring the PyPI API Token

In order to publish to PyPI, you must configure Poetry to use a PyPI API token. Once
you have the token, you will configure Poetry to use it. Poetry relies on
the Python keyring to store this secret. On MacOS and Windows, it will use the
system keyring, and no other setup is required. If you are using Debian, the
process is more complicated. See the notes below.

First, in your PyPI [account settings](https://pypi.org/manage/account/),
create an API token with upload permissions for the uciparse project.
Once you have a working keyring, configure Poetry following
the [instructions](https://python-poetry.org/docs/repositories/#configuring-credentials):

```
poetry config pypi-token.pypi <the PyPI token>
```

Note that this leaves your actual secret in the command-line history, so make sure
to scrub it once you're done.

### Python Keyring on Debian

On Debian, the process really only works from an X session. There is a way to
manipulate the keyring without being in an X session, and I used to document it
here. However, it's so ugly that I don't want to encourage anyone to use it. If
you want to dig in on your own, see the [keyring documentation](https://pypi.org/project/keyring/)
under the section **Using Keyring on headless Linux systems**.

Some setup is required to initialize the keyring in your Debian system. First,
install the `gnome-keyring` package, and then log out:

```
$ sudo apt-get install gnome-keyring
$ exit
```

Log back in and initialize your keyring by setting and then removing a dummy
value:

```
$ keyring set testvalue "user"
Password for 'user' in 'testvalue':
Please enter password for encrypted keyring:
$ keyring get testvalue "user"
Please enter password for encrypted keyring:
password
$ keyring del testvalue "user"
Deleting password for 'user' in 'testvalue':
```

At this point, the keyring should be fully functional and it should be ready
for use with Poetry. Whenever Poetry needs to read a secret from the keyring,
you'll get a popup window where you need to enter the keyring password.
> _Note:_ This process relies on a PyPI API token with upload permissions for
> the project. This token is stored in a GitHub Actions secret called
> `PYPI_TOKEN`.
14 changes: 11 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "uciparse"
version = "0.1.16"
version = "0.0.0" # published version is managed using Git tags (see below)
description = "Parse and emit OpenWRT uci-format files"
authors = ["Kenneth J. Pronovici <pronovic@ieee.org>"]
license = "Apache-2.0"
Expand All @@ -27,6 +27,14 @@ classifiers=[
"Topic :: System :: Systems Administration",
]

# Published version is managed using Git tags
# We get either the tag (like "0.24.1") or a snapshot-type version (like "0.24.1+3.e8319c4")
# If the plugin is not installed, then the version is always "0.0.0", taken from above
[tool.poetry-dynamic-versioning]
enable = true
pattern = '^[vV](?P<base>\d+\.\d+\.\d+)' # this extracts the version from our vX.Y.Z tag format
format-jinja = "{% if distance == 0 and not dirty %}{{ base }}{% else %}{{ base }}+{{ distance }}.{{ commit }}{% endif %}"

[tool.poetry.scripts]
uciparse = "uciparse.cli:parse"
ucidiff = "uciparse.cli:diff"
Expand Down Expand Up @@ -76,5 +84,5 @@ line_length = 132
skip_glob = [ "docs", "notes" ]

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
requires = ["poetry>=1.0.0", "poetry-dynamic-versioning"]
build-backend = "poetry_dynamic_versioning.backend"

0 comments on commit cb95d8f

Please sign in to comment.