From 89023001a12353943382b205970a0115130bbeea Mon Sep 17 00:00:00 2001 From: Nicholas Felt Date: Mon, 9 Sep 2024 10:36:20 -0700 Subject: [PATCH] Enhance find_unreleased_changelog_items action to check for merged PRs (#74) * build: Update license identifier for this repo * feat: Add an additional check for merged PRs to the action that finds unreleased changelog items. Also, Update the cron for releasing a package to once per month, instead of once per week. * chore: Update contributor_setup.py to quote paths to avoid shell splitting --- .github/renovate.json | 5 ++-- .github/workflows/_reusable-sbom-scan.yml | 4 +-- .github/workflows/package-release.yml | 2 +- .github/workflows/test-actions.yml | 2 ++ CHANGELOG.md | 1 + .../find_unreleased_changelog_items/main.py | 20 +++++++++------ .../find_unreleased_changelog_items/readme.md | 9 +++++-- pyproject.toml | 1 + scripts/contributor_setup.py | 10 +++++--- tests/test_find_unreleased_changelog_items.py | 25 +++++++++++++++++++ 10 files changed, 60 insertions(+), 19 deletions(-) diff --git a/.github/renovate.json b/.github/renovate.json index f78f8302..3ed3ef3a 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -12,10 +12,11 @@ "packageRules": [ { "additionalBranchPrefix": "{{#if (equals manager 'github-actions')}}gh-actions{{else}}{{categories}}{{/if}}-deps/", - "description": "Set the branch prefix for all updates", + "description": "Set the branch prefix and minimum release age for all updates", "matchPackageNames": [ "*" - ] + ], + "minimumReleaseAge": "5 days" }, { "addLabels": [ diff --git a/.github/workflows/_reusable-sbom-scan.yml b/.github/workflows/_reusable-sbom-scan.yml index eb3f9e92..c8621557 100644 --- a/.github/workflows/_reusable-sbom-scan.yml +++ b/.github/workflows/_reusable-sbom-scan.yml @@ -15,10 +15,10 @@ jobs: - uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 with: python-version-file: pyproject.toml - - name: Create lockfile + - name: Create lockfile and install dependencies run: | pip install poetry - poetry lock + poetry install - name: Create SBOM uses: anchore/sbom-action@61119d458adab75f756bc0b9e4bde25725f86a7a # v0.17.2 with: diff --git a/.github/workflows/package-release.yml b/.github/workflows/package-release.yml index 881da477..913e88a9 100644 --- a/.github/workflows/package-release.yml +++ b/.github/workflows/package-release.yml @@ -13,7 +13,7 @@ on: major for non-backward compatible changes. options: [patch, minor, major] schedule: - - cron: 0 16 * * 2 + - cron: 0 16 1-7 * 2 # Run at 16:00 UTC on the first Tuesday of each month concurrency: group: pypi jobs: diff --git a/.github/workflows/test-actions.yml b/.github/workflows/test-actions.yml index de8a77b2..f809d3df 100644 --- a/.github/workflows/test-actions.yml +++ b/.github/workflows/test-actions.yml @@ -30,6 +30,8 @@ jobs: fi test-find_unreleased_changelog_items: runs-on: ubuntu-latest + env: + UNIT_TESTING_FIND_UNRELEASED_CHANGELOG_ITEMS_ACTION: true steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Overwrite CHANGELOG.md with dummy data diff --git a/CHANGELOG.md b/CHANGELOG.md index b5a73e18..1fe93554 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ Things to be included in the next release go here. - Changed the `_reusable-update-python-and-pre-commit-dependencies.yml` workflow to no longer only work on PRs from Dependabot, users will now need to apply any conditional login in the calling workflow. - Updated the `_reusable-update-python-and-pre-commit-dependencies.yml` workflow to allow using [`renovate`](https://docs.renovatebot.com/) instead of Dependabot to update dependencies. - Updated the `_reusable-package-release.yml` workflow to not show as failed if the `bump-release` deployment is rejected by a reviewer. +- Updated the `find_unreleased_changelog_items` action to check for merged PRs since the last release and fail if none are found. --- diff --git a/actions/find_unreleased_changelog_items/main.py b/actions/find_unreleased_changelog_items/main.py index 9d48c27c..e231d2f6 100644 --- a/actions/find_unreleased_changelog_items/main.py +++ b/actions/find_unreleased_changelog_items/main.py @@ -77,6 +77,7 @@ def main() -> None: # Set the filepaths for the template files template_changelog_filepath = pathlib.Path(filepath_for_previous_changelog) template_release_notes_filepath = pathlib.Path(filepath_for_previous_release_notes) + root_dir = pathlib.Path.cwd() release_notes_content = "" found_entries = False @@ -109,6 +110,17 @@ def main() -> None: msg = f"No unreleased entries were found in {CHANGELOG_FILE}." raise SystemExit(msg) + # Check for merged PRs since the last release + run_cmd_in_subprocess( + f'git config --global --add safe.directory "{root_dir.resolve().as_posix()}"' + ) + commit_messages = get_commit_messages(since_tag=get_latest_tag()) + pr_regex = re.compile(r"\(#\d+\)$") + pr_descriptions = "\n".join([f"- {msg}" for msg in commit_messages if pr_regex.search(msg)]) + if not pr_descriptions and not os.getenv("UNIT_TESTING_FIND_UNRELEASED_CHANGELOG_ITEMS_ACTION"): + msg = "No PRs have been merged since the last release." + raise SystemExit(msg) + # Copy the files to the correct location shutil.copy(CHANGELOG_FILE, template_changelog_filepath) with template_release_notes_filepath.open("w", encoding="utf-8") as template_release_notes: @@ -117,14 +129,6 @@ def main() -> None: # If running in GitHub Actions, and the release_level is set, send the release level and # incoming changes to the GitHub Summary if release_level: - root_dir = pathlib.Path.cwd() - run_cmd_in_subprocess( - f'git config --global --add safe.directory "{root_dir.resolve().as_posix()}"' - ) - commit_messages = get_commit_messages(since_tag=get_latest_tag()) - - pr_regex = re.compile(r"\(#\d+\)$") - pr_descriptions = "\n".join([f"- {msg}" for msg in commit_messages if pr_regex.search(msg)]) summary_contents = ( f"## Workflow Inputs\n- release-level: {release_level}\n" f"## PRs Merged Since Last Release\n{pr_descriptions}\n" diff --git a/actions/find_unreleased_changelog_items/readme.md b/actions/find_unreleased_changelog_items/readme.md index 23be9324..9d42251e 100644 --- a/actions/find_unreleased_changelog_items/readme.md +++ b/actions/find_unreleased_changelog_items/readme.md @@ -2,7 +2,9 @@ This action will parse the repository's `CHANGELOG.md` file to determine if there are any unreleased items. It will fail if it cannot find any unreleased -items, as this means that the package is not ready for a new release. +items, as this means that the package is not ready for a new release. This action will also +fail if it cannot find any merged PRs since the last release, as this also means that the +package is not ready for a new release. This action will populate two files in the [`python-semantic-release` templates directory](https://python-semantic-release.readthedocs.io/en/latest/configuration.html#config-changelog-template-dir). @@ -13,7 +15,7 @@ will be used to fill in the GitHub Release Notes. > [!IMPORTANT] > This action requires that the `pyproject.toml` and `CHANGELOG.md` files exist in the -> current working directory. +> current working directory and that all tags are fetched from the remote repository. > [!IMPORTANT] > This action requires the `CHANGELOG.md` file to be in a format that is based on @@ -48,6 +50,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 + with: + fetch-depth: 0 + fetch-tags: true - uses: tektronix/python-package-ci-cd/actions/find_unreleased_changelog_items@v1.2.0 with: release-level: ${{ inputs.release-level }} # optional diff --git a/pyproject.toml b/pyproject.toml index 6b583c1d..23471289 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ wrap-descriptions = 100 wrap-summaries = 0 [tool.poetry] +license = "Apache-2.0" package-mode = false version = "1.2.0" diff --git a/scripts/contributor_setup.py b/scripts/contributor_setup.py index 6b52b7ce..956c6c86 100644 --- a/scripts/contributor_setup.py +++ b/scripts/contributor_setup.py @@ -25,7 +25,7 @@ def create_virtual_environment(virtual_env_dir: str | os.PathLike[str]) -> None: virtual_env_dir: The directory where the virtual environment should be created """ print(f"\nCreating virtualenv located at '{virtual_env_dir}'") - _run_cmd_in_subprocess(f"{sys.executable} -m venv {virtual_env_dir} --clear") + _run_cmd_in_subprocess(f'"{sys.executable}" -m venv "{virtual_env_dir}" --clear') def _run_cmd_in_subprocess(command: str) -> None: @@ -78,9 +78,11 @@ def main() -> None: ) python_executable = files[0] commands_to_send = ( - f"{python_executable} -m pip install -U pip wheel poetry", - f"{python_executable} -m poetry install", - f"{python_executable} -m pre_commit install --install-hooks", + f'"{python_executable}" -m pip install -U pip wheel poetry', + f'"{python_executable}" -m poetry install', + f'"{python_executable}" -m nodeenv --python-virtualenv --clean-src', + f'"{python_executable}" -m pre_commit install --install-hooks', + f'"{python_executable}" -m tox -e tests', ) for command in commands_to_send: _run_cmd_in_subprocess(command) diff --git a/tests/test_find_unreleased_changelog_items.py b/tests/test_find_unreleased_changelog_items.py index b6b6f103..9ac474ee 100644 --- a/tests/test_find_unreleased_changelog_items.py +++ b/tests/test_find_unreleased_changelog_items.py @@ -137,6 +137,31 @@ def test_main_no_unreleased_entries( main() +def test_main_with_no_merged_prs( + mock_env_vars: None, # noqa: ARG001 + mock_changelog_file: Path, # noqa: ARG001 + fake_process: FakeProcess, +) -> None: + """Test the main function when unreleased entries are found. + + Args: + mock_env_vars: Mock the environment variables. + mock_changelog_file: Mock the changelog file. + fake_process: The fake_process fixture, used to register commands that will be mocked. + """ + fake_process.register( # pyright: ignore[reportUnknownMemberType] + shlex.split("git log v1.0.0..HEAD --pretty=format:%s"), + stdout=b"Initial commit\n", + ) + with fake_process.context() as nested_process: + nested_process.register( # pyright: ignore[reportUnknownMemberType] + shlex.split("git log v1.0.0..HEAD --pretty=format:%s"), + stdout=b"Initial commit\n", + ) + with pytest.raises(SystemExit, match="No PRs have been merged since the last release\\."): + main() + + def test_main_with_unreleased_entries( mock_env_vars: None, # noqa: ARG001 mock_changelog_file: Path,