Skip to content

Commit

Permalink
Make galaxy[version-incorrect] rule opt-in (#2103)
Browse files Browse the repository at this point in the history
The docstring and description attribute for the GalaxyRule class have
been updated as to reflect the separation of galaxy[version-incorrect]
and that the rule does more than checking of a changelog.
  • Loading branch information
cavcrosby committed Aug 13, 2024
1 parent 5ce6ce4 commit 21fb079
Show file tree
Hide file tree
Showing 8 changed files with 182 additions and 83 deletions.
1 change: 1 addition & 0 deletions .ansible-lint
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/rules/galaxy-version-incorrect.md
2 changes: 1 addition & 1 deletion examples/.collection/galaxy.yml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
83 changes: 2 additions & 81 deletions src/ansiblelint/rules/galaxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -15,18 +14,17 @@


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)"
_ids = {
"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",
}
Expand Down Expand Up @@ -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(
Expand All @@ -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()
Expand All @@ -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"),
(
Expand Down
61 changes: 61 additions & 0 deletions src/ansiblelint/rules/galaxy_version_incorrect.md
Original file line number Diff line number Diff line change
@@ -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 <author3@example.com>"
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 <author3@example.com>"
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"
```
114 changes: 114 additions & 0 deletions src/ansiblelint/rules/galaxy_version_incorrect.py
Original file line number Diff line number Diff line change
@@ -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))
2 changes: 1 addition & 1 deletion test/test_rules_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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?"

0 comments on commit 21fb079

Please sign in to comment.