From 43aa0047a6cf917066c084216de530b9e4e69a1a Mon Sep 17 00:00:00 2001 From: Nicholas Felt Date: Tue, 17 Oct 2023 17:01:58 -0700 Subject: [PATCH] ci: Added a workflow and necessary support scripts/templates to enable automated released via GitHub's workflow_dispatch trigger. (#29) --- .github/workflows/package-release.yml | 140 ++++++++++++++++++ .pre-commit-config.yaml | 8 +- CHANGELOG.md | 11 +- CONTRIBUTING.md | 16 +- docs/troubleshooting/contributions.md | 8 +- pyproject.toml | 9 +- .../CHANGELOG.md.j2 | 23 +++ scripts/check_unreleased_changelog_items.py | 51 +++++++ 8 files changed, 247 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/package-release.yml create mode 100644 python_semantic_release_templates/CHANGELOG.md.j2 create mode 100644 scripts/check_unreleased_changelog_items.py diff --git a/.github/workflows/package-release.yml b/.github/workflows/package-release.yml new file mode 100644 index 00000000..bbec4da2 --- /dev/null +++ b/.github/workflows/package-release.yml @@ -0,0 +1,140 @@ +--- +name: Create package release and publish binaries to pypi.org +on: + workflow_dispatch: + inputs: + release_level: + type: choice + required: true + description: | + Select the release level, + patch for backward compatible minor changes and bug fixes, + minor for backward compatible larger changes, + major for non-backward compatible changes. + options: [patch, minor, major] +concurrency: + group: pypi +jobs: + pypi-version: + name: Update package version + if: github.repository == 'tektronix/tm_devices' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + environment: package-release-gate + permissions: + id-token: write + contents: write + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: x + check-latest: true + - name: Check for unreleased entries in the Changelog + run: python scripts/check_unreleased_changelog_items.py + - name: Copy Changelog to template directory + run: cp CHANGELOG.md python_semantic_release_templates/temp_CHANGELOG_copy.md + - name: Python Semantic Release + id: release + uses: python-semantic-release/python-semantic-release@v8.0.0 + with: + root_options: --verbose --strict + force: ${{ inputs.release_level }} + github_token: ${{ secrets.GITHUB_TOKEN }} + outputs: + built-version: ${{ steps.release.outputs.version }} + pypi-build: + name: Build package + needs: [pypi-version] + if: github.repository == 'tektronix/tm_devices' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Build package + uses: hynek/build-and-inspect-python-package@v1.5 + upload-testpypi: + name: Upload package to TestPyPI + needs: [pypi-build] + if: github.repository == 'tektronix/tm_devices' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + environment: package-testpypi + permissions: + id-token: write + steps: + - name: Download built packages + uses: actions/download-artifact@v3 + with: + name: Packages + path: dist + - name: Upload package to Test PyPI + uses: pypa/gh-action-pypi-publish@v1.8.8 + with: + repository-url: https://test.pypi.org/legacy/ + upload-pypi: + name: Upload package to PyPI + needs: [upload-testpypi] + if: github.repository == 'tektronix/tm_devices' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + environment: package-release + permissions: + id-token: write + steps: + - name: Download built packages + uses: actions/download-artifact@v3 + with: + name: Packages + path: dist + - name: Upload package to PyPI + uses: pypa/gh-action-pypi-publish@v1.8.8 + upload-github: + name: Upload package to GitHub Release + needs: [upload-pypi] + if: github.repository == 'tektronix/tm_devices' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + id-token: write + steps: + - name: Download built packages + uses: actions/download-artifact@v3 + with: + name: Packages + path: dist + - name: Publish package distributions to GitHub Releases + uses: python-semantic-release/upload-to-gh-release@main + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + pypi-install: + name: Install package + needs: + - pypi-version + - pypi-build + - upload-testpypi + - upload-pypi + - upload-github + if: github.repository == 'tektronix/tm_devices' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + index_urls: + - '' + - ' --index-url=https://test.pypi.org/simple/ --extra-index-url=https://pypi.org/simple' + steps: + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: x + check-latest: true + - name: Test installing package + # A retry is used to allow for some downtime before the package is installable + uses: nick-fields/retry@v2 + with: + timeout_minutes: 2 + max_attempts: 5 + retry_wait_seconds: 30 + warning_on_retry: false + command: pip install${{ matrix.index_urls }} tm_devices==${{ needs.pypi-version.outputs.built-version }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 91d7ad25..4b606955 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ ci: - pyroma repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: check-yaml - id: check-toml @@ -41,7 +41,7 @@ repos: - id: check-github-actions - id: check-github-workflows - repo: https://github.com/commitizen-tools/commitizen - rev: 3.10.0 + rev: 3.11.0 hooks: - id: commitizen stages: [commit-msg] @@ -122,12 +122,12 @@ repos: always_run: true args: [., --min=10] - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.292 + rev: v0.1.0 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/psf/black - rev: 23.9.1 + rev: 23.10.0 hooks: - id: black - repo: https://github.com/PyCQA/docformatter diff --git a/CHANGELOG.md b/CHANGELOG.md index 1335c5ea..9837b307 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org). - +Valid subsections within a version are: + +- Added +- Changed +- Deprecated +- Removed +- Fixed +- Security ______________________________________________________________________ @@ -15,7 +22,7 @@ ______________________________________________________________________ ______________________________________________________________________ -## v0.1.14 (2023-10-03) +## v0.1.14 (2023-10-05) ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 58e3ee20..2bb5a5a5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -80,10 +80,10 @@ recommended IDE for package development is ``` ```console # Linux - source .env/bin/activate + source .venv/bin/activate # Windows - .env\Scripts\activate.bat + .venv\Scripts\activate.bat ``` ```console python -m pip install -U pip poetry @@ -97,7 +97,7 @@ recommended IDE for package development is git checkout -b name-of-your-bugfix-or-feature ``` -4. Update the [CHANGELOG](CHANGELOG.md) using the proper format. +4. Update the **Unreleased** section in the [CHANGELOG](CHANGELOG.md) using the proper format. 5. When you're done making changes, check that your changes conform to any code formatting requirements and pass any tests. @@ -106,10 +106,10 @@ recommended IDE for package development is Always remember to activate the virtual environment before attempting to run tests or other code. ```console # Linux - source .env/bin/activate + source .venv/bin/activate # Windows - .env\Scripts\activate.bat + .venv\Scripts\activate.bat ``` ```` @@ -171,10 +171,10 @@ commands: Always remember to activate the virtual environment before attempting to run tests or other code. ```console # Linux -source .env/bin/activate +source .venv/bin/activate # Windows -.env\Scripts\activate.bat +.venv\Scripts\activate.bat ``` ```` @@ -194,7 +194,7 @@ Before you submit a pull request, check that it meets these guidelines: 2. If the pull request adds functionality, the docs should be updated. 3. The pull request should work for all currently supported operating systems and versions of Python. -4. The [Changelog](CHANGELOG.md) should be updated. +4. The **Unreleased** section in the [Changelog](CHANGELOG.md) should be updated. ## Project Test Plan diff --git a/docs/troubleshooting/contributions.md b/docs/troubleshooting/contributions.md index c860dcd9..e6016e48 100644 --- a/docs/troubleshooting/contributions.md +++ b/docs/troubleshooting/contributions.md @@ -213,10 +213,10 @@ then retry the command. ```console # Linux -source .env/bin/activate +source .venv/bin/activate # Windows -.env\Scripts\activate.bat +.venv\Scripts\activate.bat # Update installed dependencies python -m poetry update @@ -326,10 +326,10 @@ file when running pytest. ```console # Linux -source .env/bin/activate +source .venv/bin/activate pytest -k "test_docs" --self-contained-html --html=$(pwd)/.results_doctests/results.html # Windows -.env\Scripts\activate.bat +.venv\Scripts\activate.bat pytest -k "test_docs" --self-contained-html --html=%CD%\.results_doctests\results.html ``` diff --git a/pyproject.toml b/pyproject.toml index fa4b10eb..02722ca6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,7 +123,7 @@ pytest-cov = ">=3.0.0" pytest-html = ">=4.0" pytest-order = ">=1.0.1" pytest-profiling = ">=1.7.0" -python-semantic-release = ">=7.31.2" +python-semantic-release = ">=8.0" ruff = ">=0.0.292" safety = ">=2.1.1" sphinx-autoapi = ">=2.0.0" @@ -370,6 +370,13 @@ version_toml = [ "pyproject.toml:tool.poetry.version" ] +[tool.semantic_release.changelog] +exclude_commit_patterns = [] +template_dir = "python_semantic_release_templates" + +[tool.semantic_release.changelog.environment] +extensions = ["jinja2.ext.do"] + [tool.semantic_release.commit_parser_options] # These settings allow python-semantic-release to be used without triggering on any commits allowed_tags = [] diff --git a/python_semantic_release_templates/CHANGELOG.md.j2 b/python_semantic_release_templates/CHANGELOG.md.j2 new file mode 100644 index 00000000..5333d2f4 --- /dev/null +++ b/python_semantic_release_templates/CHANGELOG.md.j2 @@ -0,0 +1,23 @@ +{%- set latest_release_dict_entry = (context.history.released.items()|list)[0] %} +{%- set latest_version_number = latest_release_dict_entry[0].as_tag() %} +{%- set latest_version_date = latest_release_dict_entry[1].tagged_date.strftime("%Y-%m-%d") %} +{%- set recently_merged_prs = {} %} +{%- for type_, commits in latest_release_dict_entry[1]["elements"] | dictsort %} + {%- for commit in commits %} + {%- set pr_num = commit.commit.message.rstrip().rsplit("(#", 1)[-1].rsplit(")", 1)[0]|int %} + {%- if pr_num %} + {%- do recently_merged_prs.update({commit.commit.message.split("\n")[0].rsplit("(#", 1)[0]: pr_num}) %} + {%- endif %} + {%- endfor %} +{%- endfor %} + +{%- set merged_prs_text_list = ["\n\n### Merged Pull Requests\n\n"] %} +{%- for message, number in recently_merged_prs.items() %} + {%- do merged_prs_text_list.append("- " ~ message ~ "([#" + number|string ~ "](" ~ number|string|pull_request_url ~ "))\n") %} +{%- endfor %} +{%- set merged_prs_text = (merged_prs_text_list|join).rstrip() %} + + +{%- filter replace("## Unreleased", "## Unreleased\n\n______________________________________________________________________\n\n## " + latest_version_number + " (" + latest_version_date + ")" + merged_prs_text) %} + {%- include "temp_CHANGELOG_copy.md" %} +{% endfilter %} diff --git a/scripts/check_unreleased_changelog_items.py b/scripts/check_unreleased_changelog_items.py new file mode 100644 index 00000000..9015dfcf --- /dev/null +++ b/scripts/check_unreleased_changelog_items.py @@ -0,0 +1,51 @@ +"""This script will check for unreleased entries in the CHANGELOG.md file. + +It will exit with a non-zero exit code if there are no unreleased entries. +""" +import pathlib +import re + +CHANGELOG_FILENAME = "CHANGELOG.md" + + +def main() -> None: + """Check for entries in the Unreleased section of the CHANGELOG.md file. + + Raises: + SystemExit: Indicates no new entries were found. + """ + found_entries = False + with open( + pathlib.Path(__file__).parent.parent / CHANGELOG_FILENAME, encoding="utf-8" + ) as changelog_file: + tracking_unreleased = False + tracking_entries = False + for line in changelog_file: + if line.startswith("___"): + tracking_unreleased = False + tracking_entries = False + if line.startswith("## Unreleased"): + tracking_unreleased = True + if tracking_unreleased and line.startswith( + ( + "### Added\n", + "### Changed\n", + "### Deprecated\n", + "### Removed\n", + "### Fixed\n", + "### Security\n", + ) + ): + tracking_entries = True + if tracking_entries: + found_entries = bool(re.match(r"^- \w+", line)) + if found_entries: + break + + if not found_entries: + msg = f"No unreleased entries were found in {CHANGELOG_FILENAME}." + raise SystemExit(msg) + + +if __name__ == "__main__": + main()