diff --git a/.ansible-lint b/.ansible-lint index 4e92c017ee..dce6ce09a1 100644 --- a/.ansible-lint +++ b/.ansible-lint @@ -61,6 +61,7 @@ enable_list: - no-log-password # opt-in - no-same-owner # opt-in - name[prefix] # opt-in + - galaxy-version-incorrect # opt-in # add yaml here if you want to avoid ignoring yaml checks when yamllint # library is missing. Normally its absence just skips using that rule. - yaml diff --git a/docs/rules/galaxy-version-incorrect.md b/docs/rules/galaxy-version-incorrect.md new file mode 120000 index 0000000000..3ca6061890 --- /dev/null +++ b/docs/rules/galaxy-version-incorrect.md @@ -0,0 +1 @@ +../../src/ansiblelint/rules/galaxy_version_incorrect.md \ No newline at end of file diff --git a/examples/.collection/galaxy.yml b/examples/.collection/galaxy.yml index d21efb2402..52b5305c6c 100644 --- a/examples/.collection/galaxy.yml +++ b/examples/.collection/galaxy.yml @@ -1,7 +1,7 @@ --- name: foo namespace: bar -version: 0.0.0 # noqa: galaxy[version-incorrect] +version: 0.0.0 # noqa: galaxy-version-incorrect authors: - John readme: ../README.md diff --git a/mkdocs.yml b/mkdocs.yml index 10455ee7c3..7e8fd3a02a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -82,6 +82,7 @@ nav: - rules/deprecated-module.md - rules/empty-string-compare.md - rules/fqcn.md + - rules/galaxy-version-incorrect.md - rules/galaxy.md - rules/ignore-errors.md - rules/inline-env-var.md diff --git a/src/ansiblelint/rules/galaxy.py b/src/ansiblelint/rules/galaxy.py index 5775fc8157..fd4483c7bc 100644 --- a/src/ansiblelint/rules/galaxy.py +++ b/src/ansiblelint/rules/galaxy.py @@ -3,7 +3,6 @@ from __future__ import annotations import sys -from functools import total_ordering from typing import TYPE_CHECKING, Any from ansiblelint.constants import FILENAME_KEY, LINE_NUMBER_KEY @@ -15,10 +14,10 @@ class GalaxyRule(AnsibleLintRule): - """Rule for checking collection version is greater than 1.0.0 and checking for changelog.""" + """Rule for checking collections.""" id = "galaxy" - description = "Confirm via galaxy.yml file if collection version is greater than or equal to 1.0.0 and check for changelog." + description = "Confirm that collection's units are valid." severity = "MEDIUM" tags = ["metadata"] version_added = "v6.11.0 (last update)" @@ -26,7 +25,6 @@ class GalaxyRule(AnsibleLintRule): "galaxy[tags]": "galaxy.yaml must have one of the required tags", "galaxy[no-changelog]": "No changelog found. Please add a changelog file. Refer to the galaxy.md file for more info.", "galaxy[version-missing]": "galaxy.yaml should have version tag.", - "galaxy[version-incorrect]": "collection version should be greater than or equal to 1.0.0", "galaxy[no-runtime]": "meta/runtime.yml file not found.", "galaxy[invalid-dependency-version]": "Invalid collection metadata. Dependency version spec range is invalid", } @@ -120,17 +118,6 @@ def matchplay(self, file: Lintable, data: dict[str, Any]) -> list[MatchError]: # returning here as it does not make sense # to continue for version check below - version = data.get("version") - if Version(version) < Version("1.0.0"): - results.append( - self.create_matcherror( - message="collection version should be greater than or equal to 1.0.0", - lineno=version._line_number, # noqa: SLF001 - tag="galaxy[version-incorrect]", - filename=file, - ), - ) - if not (base_path / "meta" / "runtime.yml").is_file(): results.append( self.create_matcherror( @@ -143,64 +130,12 @@ def matchplay(self, file: Lintable, data: dict[str, Any]) -> list[MatchError]: return results -@total_ordering -class Version: - """Simple class to compare arbitrary versions.""" - - def __init__(self, version_string: str): - """Construct a Version object.""" - self.components = version_string.split(".") - - def __eq__(self, other: object) -> bool: - """Implement equality comparison.""" - try: - other = _coerce(other) - except NotImplementedError: - return NotImplemented - - return self.components == other.components - - def __lt__(self, other: Version) -> bool: - """Implement lower-than operation.""" - other = _coerce(other) - - return self.components < other.components - - -def _coerce(other: object) -> Version: - if isinstance(other, str): - other = Version(other) - if isinstance(other, int | float): - other = Version(str(other)) - if isinstance(other, Version): - return other - msg = f"Unable to coerce object type {type(other)} to Version" - raise NotImplementedError(msg) - - if "pytest" in sys.modules: import pytest from ansiblelint.rules import RulesCollection # pylint: disable=ungrouped-imports from ansiblelint.runner import Runner - def test_galaxy_collection_version_positive() -> None: - """Positive test for collection version in galaxy.""" - collection = RulesCollection() - collection.register(GalaxyRule()) - success = "examples/.collection/galaxy.yml" - good_runner = Runner(success, rules=collection) - assert good_runner.run() == [] - - def test_galaxy_collection_version_negative() -> None: - """Negative test for collection version in galaxy.""" - collection = RulesCollection() - collection.register(GalaxyRule()) - failure = "examples/meta/galaxy.yml" - bad_runner = Runner(failure, rules=collection) - errs = bad_runner.run() - assert len(errs) == 1 - def test_galaxy_no_collection_version() -> None: """Test for no collection version in galaxy.""" collection = RulesCollection() @@ -210,20 +145,6 @@ def test_galaxy_no_collection_version() -> None: errs = bad_runner.run() assert len(errs) == 1 - def test_version_class() -> None: - """Test for version class.""" - v = Version("1.0.0") - assert v == Version("1.0.0") - assert v != NotImplemented - - def test_coerce() -> None: - """Test for _coerce function.""" - assert _coerce("1.0") == Version("1.0") - assert _coerce(1.0) == Version("1.0") - expected = "Unable to coerce object type" - with pytest.raises(NotImplementedError, match=expected): - _coerce(type(Version)) - @pytest.mark.parametrize( ("file", "expected"), ( diff --git a/src/ansiblelint/rules/galaxy_version_incorrect.md b/src/ansiblelint/rules/galaxy_version_incorrect.md new file mode 100644 index 0000000000..28cc366c50 --- /dev/null +++ b/src/ansiblelint/rules/galaxy_version_incorrect.md @@ -0,0 +1,61 @@ +# galaxy-version-incorrect + +This rule checks that the `version` key within `galaxy.yml` is greater than or +equal to `1.0.0`. This is to follow semantic versioning standards that are +enforced in the Ansible Automation Platform. + +This is an opt-in rule. You must enable it in your Ansible-lint configuration as +follows: + +```yaml +enable_list: + - galaxy-version-incorrect +``` + +## Problematic Code + +```yaml +description: "description" +namespace: "namespace_name" +name: "collection_name" +version: "0.0.1" # <- version key is not greater than or equal to '1.0.0'. +readme: "README.md" +authors: + - "Author1" + - "Author2 (https://author2.example.com)" + - "Author3 " +dependencies: + "other_namespace.collection1": ">=1.0.0" + "other_namespace.collection2": ">=2.0.0,<3.0.0" + "anderson55.my_collection": "*" # note: "*" selects the highest version available +license: + - "MIT" +tags: + - demo + - collection +repository: "https://www.github.com/my_org/my_collection" +``` + +## Correct Code + +```yaml +description: "description" +namespace: "namespace_name" +name: "collection_name" +version: "1.0.0" # <- version key is greater than or equal to '1.0.0'. +readme: "README.md" +authors: + - "Author1" + - "Author2 (https://author2.example.com)" + - "Author3 " +dependencies: + "other_namespace.collection1": ">=1.0.0" + "other_namespace.collection2": ">=2.0.0,<3.0.0" + "anderson55.my_collection": "*" # note: "*" selects the highest version available +license: + - "MIT" +tags: + - demo + - collection +repository: "https://www.github.com/my_org/my_collection" +``` diff --git a/src/ansiblelint/rules/galaxy_version_incorrect.py b/src/ansiblelint/rules/galaxy_version_incorrect.py new file mode 100644 index 0000000000..165e5274cc --- /dev/null +++ b/src/ansiblelint/rules/galaxy_version_incorrect.py @@ -0,0 +1,114 @@ +"""Implementation of GalaxyVersionIncorrectRule.""" + +from __future__ import annotations + +import sys +from functools import total_ordering +from typing import TYPE_CHECKING, Any + +from ansiblelint.rules import AnsibleLintRule + +if TYPE_CHECKING: + from ansiblelint.errors import MatchError + from ansiblelint.file_utils import Lintable + + +class GalaxyVersionIncorrectRule(AnsibleLintRule): + """Rule for checking collection version is greater than 1.0.0.""" + + id = "galaxy-version-incorrect" + description = "Confirm via galaxy.yml file if collection version is greater than or equal to 1.0.0." + severity = "MEDIUM" + tags = ["opt-in", "metadata"] + version_added = "v24.7.0" + + def matchplay(self, file: Lintable, data: dict[str, Any]) -> list[MatchError]: + """Return matches found for a specific play (entry in playbook).""" + if file.kind != "galaxy": # type: ignore[comparison-overlap] + return [] + + results = [] + version = data.get("version") + if Version(version) < Version("1.0.0"): + results.append( + self.create_matcherror( + message="collection version should be greater than or equal to 1.0.0", + lineno=version._line_number, # noqa: SLF001 + filename=file, + ), + ) + + return results + + +@total_ordering +class Version: + """Simple class to compare arbitrary versions.""" + + def __init__(self, version_string: str): + """Construct a Version object.""" + self.components = version_string.split(".") + + def __eq__(self, other: object) -> bool: + """Implement equality comparison.""" + try: + other = _coerce(other) + except NotImplementedError: + return NotImplemented + + return self.components == other.components + + def __lt__(self, other: Version) -> bool: + """Implement lower-than operation.""" + other = _coerce(other) + + return self.components < other.components + + +def _coerce(other: object) -> Version: + if isinstance(other, str): + other = Version(other) + if isinstance(other, int | float): + other = Version(str(other)) + if isinstance(other, Version): + return other + msg = f"Unable to coerce object type {type(other)} to Version" + raise NotImplementedError(msg) + + +if "pytest" in sys.modules: + import pytest + + from ansiblelint.rules import RulesCollection # pylint: disable=ungrouped-imports + from ansiblelint.runner import Runner + + def test_galaxy_collection_version_positive() -> None: + """Positive test for collection version in galaxy.""" + collection = RulesCollection() + collection.register(GalaxyVersionIncorrectRule()) + success = "examples/.collection/galaxy.yml" + good_runner = Runner(success, rules=collection) + assert good_runner.run() == [] + + def test_galaxy_collection_version_negative() -> None: + """Negative test for collection version in galaxy.""" + collection = RulesCollection() + collection.register(GalaxyVersionIncorrectRule()) + failure = "examples/meta/galaxy.yml" + bad_runner = Runner(failure, rules=collection) + errs = bad_runner.run() + assert len(errs) == 1 + + def test_version_class() -> None: + """Test for version class.""" + v = Version("1.0.0") + assert v == Version("1.0.0") + assert v != NotImplemented + + def test_coerce() -> None: + """Test for _coerce function.""" + assert _coerce("1.0") == Version("1.0") + assert _coerce(1.0) == Version("1.0") + expected = "Unable to coerce object type" + with pytest.raises(NotImplementedError, match=expected): + _coerce(type(Version)) diff --git a/test/test_rules_collection.py b/test/test_rules_collection.py index 44317fe3b8..0461cfaaa3 100644 --- a/test/test_rules_collection.py +++ b/test/test_rules_collection.py @@ -175,5 +175,5 @@ def test_rules_id_format(config_options: Options) -> None: rule.help or rule.description or rule.__doc__ ), f"Rule {rule.id} must have at least one of: .help, .description, .__doc__" assert "yaml" in keys, "yaml rule is missing" - assert len(rules) == 50 # update this number when adding new rules! + assert len(rules) == 51 # update this number when adding new rules! assert len(keys) == len(rules), "Duplicate rule ids?"