Skip to content

Commit

Permalink
Automate the release process trigger weekly (#68)
Browse files Browse the repository at this point in the history
* chore: Update renovate config to separate minor and patch releases to enable automerge on all patch releases

* chore: Create a packageRule that allows automerge for minor versions of certain packages, primarily packages that use date versioning like certifi

* feat: Update the `find_unreleased_changelog_items` action to add all PRs merged since the last release to the GitHub Job summary for the review of release

* ci: Update changelog template to always include an entry for updating the dependencies

* fix: Update command execution method to provide insight as action is running

* ci: Automate the release workflow to run each week with a patch release

* chore: Group together all pydantic dependencies, since they will usually fail to update in isolation
  • Loading branch information
nfelt14 authored Sep 5, 2024
1 parent a4e04d5 commit 392ba5d
Show file tree
Hide file tree
Showing 10 changed files with 132 additions and 8 deletions.
16 changes: 15 additions & 1 deletion .github/renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"dependencyDashboard": true,
"extends": [
"config:best-practices",
"group:githubArtifactActions"
"group:githubArtifactActions",
":separatePatchReleases"
],
"ignoreDeps": [
"tektronix/python-package-ci-cd"
Expand Down Expand Up @@ -75,6 +76,19 @@
"patch"
]
},
{
"description": "Allow automerge for minor updates of certain packages",
"matchPackageNames": [
"certifi"
]
},
{
"description": "Group together all pydantic dependencies",
"groupName": "pydantic dependencies",
"matchPackageNames": [
"/^pydantic/"
]
},
{
"automerge": false,
"description": "Group together all python-semantic-release dependencies",
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/_reusable-package-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ jobs:
contents: read
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
fetch-depth: 0
fetch-tags: true
- name: python-versions-array input missing
if: ${{ inputs.build-and-publish-python-package == true && (inputs.python-versions-array == null || inputs.python-versions-array == '') }}
run: |
Expand Down Expand Up @@ -127,6 +130,7 @@ jobs:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
fetch-depth: 0
fetch-tags: true
token: ${{ secrets.checkout-token }}
- if: ${{ endsWith(github.repository, '/python-package-ci-cd') }} # Run the local action when this is run in the python-package-ci-cd repository
uses: ./actions/find_unreleased_changelog_items
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/package-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ on:
minor for backward compatible larger changes,
major for non-backward compatible changes.
options: [patch, minor, major]
schedule:
- cron: 0 16 * * 2
concurrency:
group: pypi
jobs:
Expand All @@ -23,7 +25,7 @@ jobs:
build-and-publish-python-package: false
commit-user-name: ${{ vars.TEK_OPENSOURCE_NAME }}
commit-user-email: ${{ vars.TEK_OPENSOURCE_EMAIL }}
release-level: ${{ inputs.release-level }}
release-level: ${{ inputs.release-level || 'patch' }}
previous-changelog-filepath: python_semantic_release_templates/.previous_changelog_for_template.md
previous-release-notes-filepath: python_semantic_release_templates/.previous_release_notes_for_template.md
permissions:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/test-actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ jobs:
MULTILINE_STRING=$(cat <<'EOF'
## Workflow Inputs
- release-level: patch
## PRs Merged Since Last Release
## Incoming Changes
Things to be included in the next release go here.
### Added
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ Things to be included in the next release go here.
### Added

- Added the ability for the `update_development_dependencies` action to accept a comma-separated, multiline string
- Added all PRs merged since the last release to the job summary for the release workflow

### Changed

- Bumped dependency versions.
- 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.

Expand Down
3 changes: 3 additions & 0 deletions actions/find_unreleased_changelog_items/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ COPY requirements.txt /requirements.txt
COPY main.py /main.py

# Install dependencies
RUN apk update && \
apk add --no-cache git && \
rm -rf /var/cache/apk/*
RUN python -m pip install --no-cache-dir --requirement /requirements.txt

# Run the Python script as the entrypoint
Expand Down
52 changes: 51 additions & 1 deletion actions/find_unreleased_changelog_items/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,52 @@
import os
import pathlib
import re
import shlex
import shutil
import subprocess

CHANGELOG_FILE = pathlib.Path("./CHANGELOG.md")


def run_cmd_in_subprocess(command: str) -> str:
"""Run the given command in a subprocess and return the result.
Args:
command: The command string to send.
Returns:
The output from the command.
"""
command = command.replace("\\", "/")
print(f"\nExecuting command: {command}")
return subprocess.check_output(shlex.split(command)).decode() # noqa: S603


def get_latest_tag() -> str | None:
"""Retrieve the latest tag in the Git repository.
Returns:
The latest tag as a string if it exists, otherwise None.
"""
try:
return run_cmd_in_subprocess("git describe --tags --abbrev=0").strip()
except subprocess.CalledProcessError:
return None


def get_commit_messages(since_tag: str | None = None) -> list[str]:
"""Retrieve commit messages from the Git repository.
Args:
since_tag: The tag from which to start listing commits. If None, lists all commits.
Returns:
A list of commit messages as strings.
"""
range_spec = f"{since_tag}..HEAD" if since_tag else "HEAD"
return run_cmd_in_subprocess(f"git log {range_spec} --pretty=format:%s").splitlines()


def main() -> None:
"""Check for entries in the Unreleased section of the CHANGELOG.md file.
Expand Down Expand Up @@ -76,12 +117,21 @@ 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"
f"## Incoming Changes\n{release_notes_content.replace('## Unreleased', '').strip()}\n"
)
print(
f"Adding the following contents to the GitHub Workflow Summary:\n\n{summary_contents}"
f"\nAdding the following contents to the GitHub Workflow Summary:\n\n{summary_contents}"
)
with open(os.environ["GITHUB_STEP_SUMMARY"], "a") as summary_file: # noqa: PTH123
summary_file.write(summary_contents)
Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ pytest-env = "^1.1.3"
pytest-github-report = "^0.0.1"
pytest-html = "^4.1.1"
pytest-order = "^1.2.1"
pytest-subprocess = "^1.5.2"

[tool.pyright]
ignore = [
Expand Down Expand Up @@ -109,7 +110,8 @@ junit_family = "xunit2"
junit_logging = "all"
markers = [
'docs',
'order'
'order',
'slow'
]
pythonpath = "."
xfail_strict = true
Expand Down
2 changes: 1 addition & 1 deletion python_semantic_release_templates/CHANGELOG.md.j2
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{%- import ".macros.j2" as macros %}

{%- call(output) macros.populate_variables() %}
{%- filter replace("## Unreleased", "## Unreleased\n\nThings to be included in the next release go here.\n\n---\n\n## " + output[0] + " (" + output[1] + ")" + output[2]) %}
{%- filter replace("## Unreleased", "## Unreleased\n\nThings to be included in the next release go here.\n\n### Changed\n\n- Bumped dependency versions.\n\n---\n\n## " + output[0] + " (" + output[1] + ")" + output[2]) %}
{%- filter replace("Things to be included in the next release go here.\n\n", "") %}
{%- include ".previous_changelog_for_template.md" %}
{%- endfilter %}
Expand Down
51 changes: 48 additions & 3 deletions tests/test_find_unreleased_changelog_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@

from __future__ import annotations

import shlex
import subprocess

from typing import TYPE_CHECKING
from unittest import mock

import pytest

from actions.find_unreleased_changelog_items.main import main
from actions.find_unreleased_changelog_items.main import get_commit_messages, get_latest_tag, main

if TYPE_CHECKING:
from pathlib import Path

from pytest_subprocess import FakeProcess

PREVIOUS_CHANGELOG_FILEPATH = "previous_changelog.md"
PREVIOUS_RELEASE_NOTES_FILEPATH = "previous_release_notes.md"
MOCK_TEMPLATES_FOLDER = "mock_templates"
Expand Down Expand Up @@ -76,15 +82,28 @@ def mock_env_vars(
monkeypatch: pytest.MonkeyPatch,
summary_file: Path,
mock_previous_files: tuple[Path, Path],
fake_process: FakeProcess,
) -> None:
"""Mock the environment variables to simulate GitHub Actions inputs.
This fixture also mocks subprocess.check_output to enable testing to function without running
git commands.
Args:
tmp_path: The temporary path fixture.
monkeypatch: The monkeypatch fixture.
summary_file: The path to the job summary file.
mock_previous_files: Paths to the previous changelog file and previous release notes file.
fake_process: The fake_process fixture, used to register commands that will be mocked.
"""
fake_process.register( # pyright: ignore[reportUnknownMemberType]
shlex.split(f"git config --global --add safe.directory {tmp_path.resolve().as_posix()}")
)
fake_process.register(shlex.split("git describe --tags --abbrev=0"), stdout=b"v1.0.0\n") # pyright: ignore[reportUnknownMemberType]
fake_process.register( # pyright: ignore[reportUnknownMemberType]
shlex.split("git log v1.0.0..HEAD --pretty=format:%s"),
stdout=b"Initial commit\nAdd new feature (#123)\n",
)
# Change the working directory
monkeypatch.chdir(tmp_path)
monkeypatch.setenv("INPUT_PREVIOUS-CHANGELOG-FILEPATH", mock_previous_files[0].as_posix())
Expand Down Expand Up @@ -139,8 +158,17 @@ def test_main_with_unreleased_entries(

with summary_file.open("r") as summary_file_handle:
summary_contents = summary_file_handle.read()
assert "## Workflow Inputs\n- release-level: minor\n" in summary_contents
assert "## Incoming Changes\n### Added\n- New feature" in summary_contents
assert (
summary_contents
== """## Workflow Inputs
- release-level: minor
## PRs Merged Since Last Release
- Add new feature (#123)
## Incoming Changes
### Added
- New feature
"""
)


def test_main_with_no_release_level(
Expand All @@ -166,3 +194,20 @@ def test_main_with_no_release_level(
assert mock_previous_files[0].read_text() == mock_changelog_file.read_text()
assert mock_previous_files[1].read_text().strip() == "## Unreleased\n### Added\n- New feature"
assert not summary_file.exists()


def test_get_latest_tag() -> None:
"""Test the get_latest_tag function."""
with mock.patch("subprocess.check_output") as mock_check_output:
mock_check_output.return_value = b"v1.0.0\n"
assert get_latest_tag() == "v1.0.0"

mock_check_output.side_effect = subprocess.CalledProcessError(1, "git")
assert get_latest_tag() is None


def test_get_commit_messages() -> None:
"""Test the get_commit_messages function."""
with mock.patch("subprocess.check_output") as mock_check_output:
mock_check_output.return_value = b"Initial commit\nAdd new feature\n"
assert get_commit_messages() == ["Initial commit", "Add new feature"]

0 comments on commit 392ba5d

Please sign in to comment.