Skip to content

Commit

Permalink
Merge pull request #7 from stfc/check-labels-and-version
Browse files Browse the repository at this point in the history
Check labels and version
  • Loading branch information
khalford authored Feb 5, 2025
2 parents eec048e + 24c11e0 commit c194c99
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 16 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@


This action compares the app version number from your working branch to the main branch.
The pull request must have one of the below labels matching the version change.<br>

`major | minor | bug | patch`

You can also check that the **first** image version that appears in your `docker-compose.yaml` file matches the app version

Expand All @@ -20,9 +23,6 @@ More detailed information about the versions can be found [here](https://packagi
As of October 2024 GitHub actions using Docker Containers can only be run on GitHub runners using a Linux operating system.<br>
Read here for details: [Link to GitHub docs](https://docs.github.com/en/actions/sharing-automations/creating-actions/about-custom-actions#types-of-actions)

The release tag is extracted and stored in `$GITHUB_ENV`,
you can access this in your workflow with `$ {{ env.release_tag }}`

If you are making a change which should not affect the version such as README or CI changes. You can label the pull request with `documentation` or `workflow` and the version checks will be skipped.

<!-- start usage -->
Expand All @@ -47,11 +47,11 @@ If you are making a change which should not affect the version such as README or
id: version_comparison
uses: stfc/check-version-action@main
with:
labels: ${{ toJSON(github.event.pull_request.labels.*.name) }}
# Path to version file from project root
app_version_path: "version.txt"
# Optional: to check if Docker compose image version matches app version
docker_compose_path: "docker-compose.yaml"
labels: ${{ toJSON(github.event.pull_request.labels.*.name) }}
```
<!-- end usage -->
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
services:
self-test:
image: some/test:1.2.1
image: some/test:1.3.1
32 changes: 31 additions & 1 deletion src/features/app_version.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,36 @@
"""Compare app version.txt on main to the branch."""

from pathlib import Path
from typing import List

from packaging.version import Version


class CompareAppVersion:
"""This class compares the app versions"""

def run(self, path1: Path, path2: Path) -> bool:
def run(self, path1: Path, path2: Path, labels: List[str]) -> bool:
"""
Entry point to compare app versions.
:param path1: Path to main version
:param path2: Path to branch version
:param labels: Labels provided in the pull request
:return: true if success, error if fail
"""
main_content, branch_content = self.read_files(path1, path2)
main_ver = self.get_version(main_content)
branch_ver = self.get_version(branch_content)
comparison = self.compare(main_ver, branch_ver)
same_as_label = self.check_label(labels, main_ver, branch_ver)
if not comparison:
raise RuntimeError(
f"The version in {('/'.join(str(path2).split('/')[4:]))[0:]} has not been updated correctly."
)
if not same_as_label:
raise RuntimeError(
f"The version in {('/'.join(str(path2).split('/')[4:]))[0:]} "
f"does not reflect the labels on the pull request."
)
return True

@staticmethod
Expand Down Expand Up @@ -58,3 +66,25 @@ def compare(main: Version, branch: Version) -> bool:
:return: If the version update is correct return true, else return error
"""
return branch > main

@staticmethod
def check_label(
labels: List[str], main_version: Version, branch_version: Version
) -> bool:
"""
Check that the semver change used in the labels is the same as the actual change.
:param labels: Labels on the pull request
:param main_version: Version on the main branch
:param branch_version: Version on the pull request branch
:return: If change is the same or not.
"""
if main_version.major != branch_version.major:
if "major" not in labels:
return False
if main_version.minor != branch_version.minor:
if "minor" not in labels:
return False
if main_version.micro != branch_version.micro:
if "bug" not in labels and "patch" not in labels:
return False
return True
5 changes: 3 additions & 2 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ def main() -> bool:
Here we get environment variables then set environment variables when finished.
"""
# Check if the action should skip version checks
for label in os.environ.get("INPUT_LABELS"):
labels = os.environ.get("INPUT_LABELS")
for label in labels:
if label in ["documentation", "workflow"]:
return False

Expand All @@ -24,7 +25,7 @@ def main() -> bool:
branch_path = root_path / "branch"

# Action must compare the app version as the minimum feature.
CompareAppVersion().run(main_path / app_path, branch_path / app_path)
CompareAppVersion().run(main_path / app_path, branch_path / app_path, labels)

# Compare the Docker compose file version if given
if compose_path:
Expand Down
49 changes: 43 additions & 6 deletions tests/test_app_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,33 +13,50 @@ def instance_fixture():
return CompareAppVersion()


@patch("features.app_version.CompareAppVersion.check_label")
@patch("features.app_version.CompareAppVersion.compare")
@patch("features.app_version.CompareAppVersion.get_version")
@patch("features.app_version.CompareAppVersion.read_files")
def test_run(mock_read, mock_get_version, mock_compare, instance):
def test_run(mock_read, mock_get_version, mock_compare, mock_check_label, instance):
"""Test the run method makes correct calls."""
mock_path1 = Path("mock1")
mock_path2 = Path("mock2")
mock_read.return_value = ("1.0.0", "1.0.1")
mock_get_version.side_effect = [Version("1.0.0"), Version("1.0.0")]
mock_compare.return_value = True
res = instance.run(mock_path1, mock_path2)
res = instance.run(mock_path1, mock_path2, ["mock_label"])
mock_read.assert_called_once_with(mock_path1, mock_path2)
mock_get_version.assert_any_call("1.0.0")
mock_get_version.assert_any_call("1.0.1")
mock_compare.assert_called_once_with(Version("1.0.0"), Version("1.0.0"))
mock_check_label.assert_called_once_with(
["mock_label"], Version("1.0.0"), Version("1.0.0")
)
assert res


@patch("features.app_version.CompareAppVersion.compare")
@patch("features.app_version.CompareAppVersion.get_version")
@patch("features.app_version.CompareAppVersion.read_files")
def test_run_fails(mock_read, _, mock_compare, instance):
"""Test the run method fails."""
def test_run_fails_comparison(mock_read, _, mock_compare, instance):
"""Test the run method fails on the comparison check."""
mock_read.return_value = ("mock1", "mock2")
mock_compare.side_effect = RuntimeError()
mock_compare.return_value = False
with pytest.raises(RuntimeError):
instance.run(Path("mock1"), Path("mock2"))
instance.run(Path("mock1"), Path("mock2"), [])


@patch("features.app_version.CompareAppVersion.check_label")
@patch("features.app_version.CompareAppVersion.compare")
@patch("features.app_version.CompareAppVersion.get_version")
@patch("features.app_version.CompareAppVersion.read_files")
def test_run_fails_check_label(mock_read, _, mock_compare, mock_check_label, instance):
"""Test the run method fails on the label check."""
mock_read.return_value = ("mock1", "mock2")
mock_check_label.return_value = False
mock_compare.return_value = True
with pytest.raises(RuntimeError):
instance.run(Path("mock1"), Path("mock2"), [])


def test_read_files(instance):
Expand All @@ -65,3 +82,23 @@ def test_compare_fails(instance):
"""Test that the compare returns an error for an invalid features.app_version"""
res = instance.compare(Version("1.0.1"), Version("1.0.0"))
assert not res


def test_check_label(instance):
"""Test that the check passes for a correct version change"""
assert instance.check_label(["major"], Version("1.0.0"), Version("2.0.0"))


def test_check_label_fails_major(instance):
"""Test the function returns false when a major change is made but not labeled."""
assert not instance.check_label(["minor"], Version("1.0.0"), Version("2.0.0"))


def test_check_label_fails_minor(instance):
"""Test the function returns false when a minor change is made but not labeled."""
assert not instance.check_label(["major"], Version("1.0.0"), Version("1.1.0"))


def test_check_label_fails_micro(instance):
"""Test the function returns false when a micro change is made but not labeled."""
assert not instance.check_label(["minor"], Version("1.0.0"), Version("1.0.1"))
2 changes: 1 addition & 1 deletion tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def test_main(mock_os, mock_compare_app, mock_compare_compose):
mock_branch_path = Path("workspace") / "branch"
mock_main_path = Path("workspace") / "main"
mock_compare_app.return_value.run.assert_called_once_with(
mock_main_path / "app", mock_branch_path / "app"
mock_main_path / "app", mock_branch_path / "app", ["some_label"]
)
mock_compare_compose.return_value.run.assert_called_once_with(
mock_branch_path / "app", mock_branch_path / "compose"
Expand Down
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.2.1
1.3.1

0 comments on commit c194c99

Please sign in to comment.