From cb95d8f0d058bc6c36eb4a3cad340c160779cbbe Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Fri, 30 Dec 2022 19:29:53 +0000 Subject: [PATCH] Automate the existing manual release process using GitHub Actions --- .github/workflows/test-suite.yml | 21 ++++++-- .run/commands/bumpchangelog.sh | 13 +++++ .run/commands/publishpypi.sh | 28 ---------- .run/commands/tagrelease.sh | 33 ++++-------- .run/tasks/dch.sh | 12 +++++ .run/tasks/publish.sh | 10 ---- .run/tasks/release.sh | 2 +- .run/util.sh | 16 ++++++ Changelog | 1 + DEVELOPER.md | 89 +++++--------------------------- pyproject.toml | 14 +++-- 11 files changed, 95 insertions(+), 144 deletions(-) create mode 100644 .run/commands/bumpchangelog.sh delete mode 100644 .run/commands/publishpypi.sh create mode 100644 .run/tasks/dch.sh delete mode 100644 .run/tasks/publish.sh diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index eecb93c..8e2a856 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -5,6 +5,8 @@ on: push: branches: - master + tags: + - "v*" pull_request: branches: - master @@ -15,7 +17,8 @@ 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' ]" @@ -23,7 +26,8 @@ jobs: 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' ]" @@ -31,10 +35,21 @@ jobs: 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 diff --git a/.run/commands/bumpchangelog.sh b/.run/commands/bumpchangelog.sh new file mode 100644 index 0000000..9b9b9fe --- /dev/null +++ b/.run/commands/bumpchangelog.sh @@ -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.$$ +} + diff --git a/.run/commands/publishpypi.sh b/.run/commands/publishpypi.sh deleted file mode 100644 index 62ac33e..0000000 --- a/.run/commands/publishpypi.sh +++ /dev/null @@ -1,28 +0,0 @@ -# vim: set ft=bash ts=3 sw=3 expandtab: -# Publish the current tagged code to PyPI and push to GitHub - -# Before doing this, you must retrieve and configure a local API token -# For instance: poetry config pypi-token.pypi token --local -# See: https://python-poetry.org/docs/repositories/#configuring-credentials - -command_publishpypi() { - run_command enablekeyring # or we can't get the token from the keyring - - poetry build - if [ $? != 0 ]; then - echo "*** Build step failed." - exit 1 - fi - - poetry publish - if [ $? != 0 ]; then - echo "*** Publish step failed." - exit 1 - fi - - git push --follow-tags - if [ $? != 0 ]; then - exit 1 - fi -} - diff --git a/.run/commands/tagrelease.sh b/.run/commands/tagrelease.sh index 4f7e157..9383fd5 100644 --- a/.run/commands/tagrelease.sh +++ b/.run/commands/tagrelease.sh @@ -1,8 +1,8 @@ # 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 " @@ -10,20 +10,10 @@ command_tagrelease() { 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" @@ -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 @@ -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 "" } diff --git a/.run/tasks/dch.sh b/.run/tasks/dch.sh new file mode 100644 index 0000000..9f4182c --- /dev/null +++ b/.run/tasks/dch.sh @@ -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 +} + diff --git a/.run/tasks/publish.sh b/.run/tasks/publish.sh deleted file mode 100644 index 21f28ab..0000000 --- a/.run/tasks/publish.sh +++ /dev/null @@ -1,10 +0,0 @@ -# vim: set ft=bash sw=3 ts=3 expandtab: - -help_publish() { - echo "- run publish: Publish the current code to PyPI and push to GitHub" -} - -task_publish() { - run_command publishpypi -} - diff --git a/.run/tasks/release.sh b/.run/tasks/release.sh index bdf3f7a..c016744 100644 --- a/.run/tasks/release.sh +++ b/.run/tasks/release.sh @@ -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() { diff --git a/.run/util.sh b/.run/util.sh index 65337f7..4213e6a 100644 --- a/.run/util.sh +++ b/.run/util.sh @@ -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 diff --git a/Changelog b/Changelog index 81a1c15..5392779 100644 --- a/Changelog +++ b/Changelog @@ -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 diff --git a/DEVELOPER.md b/DEVELOPER.md index 50ba7fb..b6013ce 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -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 @@ -359,8 +358,8 @@ 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 @@ -368,9 +367,7 @@ 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`. @@ -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 -``` - -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`. diff --git a/pyproject.toml b/pyproject.toml index 8eaff34..b87a76d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 "] license = "Apache-2.0" @@ -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\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" @@ -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"