diff --git a/docs/_autofix_rules.md b/docs/_autofix_rules.md index 15de3bb755..5d3cf125b7 100644 --- a/docs/_autofix_rules.md +++ b/docs/_autofix_rules.md @@ -8,4 +8,5 @@ - [no-jinja-when](rules/no-jinja-when.md) - [no-log-password](rules/no-log-password.md) - [partial-become](rules/partial-become.md) +- [pattern](rules/pattern.md) - [yaml](rules/yaml.md) diff --git a/examples/collections/extensions/patterns/transform_pattern/meta/pattern.json b/examples/collections/extensions/patterns/transform_pattern/meta/pattern.json new file mode 100644 index 0000000000..847a9a1ca8 --- /dev/null +++ b/examples/collections/extensions/patterns/transform_pattern/meta/pattern.json @@ -0,0 +1,44 @@ +{ + "schema_version": "1.0", + "name": "wrong_name", + "title": "Weather Forecasting", + "description": "This pattern is designed to help get the weather forecast for a given airport code. It creates a project, EE, and job templates in automation controller to get the weather forecast.", + "short_description": "This pattern is designed to help get the weather forecast for a given airport code.", + "tags": ["weather", "forecasting"], + "aap_resources": { + "controller_project": { + "name": "Weather Forecasting", + "description": "Project for the Weather Forecasting pattern" + }, + "controller_execution_environment": { + "name": "Weather Forecasting", + "description": "EE for the Weather Forecasting pattern", + "image_name": "weather-demo-ee", + "pull": "missing" + }, + "controller_labels": ["weather", "forecasting"], + "controller_job_templates": [ + { + "name": "Get Weather Forecast", + "description": "This job template gets the weather at the location of a provided airport code.", + "execution_environment": "Weather Forecasting", + "playbook": "site.yml", + "primary": true, + "labels": ["weather", "forecasting"], + "survey": { + "name": "Weather Forecasting", + "description": "Survey to configure the weather forecasting pattern", + "spec": [ + { + "type": "text", + "question_name": "Location", + "question_description": "Enter the airport code for which you want to get the weather forecast", + "variable": "location", + "required": true + } + ] + } + } + ] + } +} diff --git a/src/ansiblelint/app.py b/src/ansiblelint/app.py index fa0c67d400..28d5bac9f9 100644 --- a/src/ansiblelint/app.py +++ b/src/ansiblelint/app.py @@ -299,7 +299,8 @@ def report_summary( # pylint: disable=too-many-locals # noqa: C901 summary.sort() if changed_files_count: - console_stderr.print(f"Modified {changed_files_count} files.") + file_word = "file" if changed_files_count == 1 else "files" + console_stderr.print(f"Modified {changed_files_count} {file_word}.") # determine which profile passed summary.passed_profile = "" diff --git a/src/ansiblelint/rules/pattern.py b/src/ansiblelint/rules/pattern.py index b2e87ea182..94f146e4a5 100644 --- a/src/ansiblelint/rules/pattern.py +++ b/src/ansiblelint/rules/pattern.py @@ -3,18 +3,25 @@ from __future__ import annotations import json +import os import sys from pathlib import Path from typing import TYPE_CHECKING +from unittest import mock -from ansiblelint.rules import AnsibleLintRule +from ansiblelint.rules import AnsibleLintRule, TransformMixin +from ansiblelint.runner import get_matches +from ansiblelint.transformer import Transformer if TYPE_CHECKING: + from ruamel.yaml.comments import CommentedMap, CommentedSeq + + from ansiblelint.config import Options from ansiblelint.errors import MatchError from ansiblelint.file_utils import Lintable -class PatternRule(AnsibleLintRule): +class PatternRule(AnsibleLintRule, TransformMixin): """Rule for checking pattern directory.""" id = "pattern" @@ -104,6 +111,30 @@ def matchyaml(self, file: Lintable) -> list[MatchError]: return results + def transform( + self, + match: MatchError, + lintable: Lintable, + data: CommentedMap | CommentedSeq | str, + ) -> None: + """Transform pattern.json to fix name-mismatch validation issues.""" + if match.tag == f"{self.id}[name-mismatch]": + # Get the pattern directory name from the file path + # pattern.json should be located at: pattern_dir/meta/pattern.json + pattern_dir = lintable.path.parent.parent.name + + # For JSON files, data is a string, so we need to parse it + if isinstance(data, str): + pattern_data = json.loads(data) + pattern_data["name"] = pattern_dir + lintable.content = json.dumps(pattern_data, indent=2) + else: + # For YAML files, data is CommentedMap/CommentedSeq + # This shouldn't happen for pattern.json, but just in case + data["name"] = pattern_dir + + match.fixed = True + def values_from_pattern_json(file: Path) -> list[str]: """Extract playbook name and pattern name values from pattern.json file.""" @@ -164,3 +195,36 @@ def test_pattern( for index, result in enumerate(results): assert result.rule.id == PatternRule.id, result assert result.tag == expected[index] + + @pytest.mark.libyaml + @mock.patch.dict(os.environ, {"ANSIBLE_LINT_WRITE_TMP": "1"}, clear=True) + def test_pattern_transform( + config_options: Options, + ) -> None: + """Test transform functionality for pattern rule.""" + pattern_file = Path( + "examples/collections/extensions/patterns/transform_pattern/meta/pattern.json" + ) + config_options.write_list = ["pattern"] + rules = RulesCollection(options=config_options) + rules.register(PatternRule()) + + config_options.lintables = [str(pattern_file)] + runner_result = get_matches(rules=rules, options=config_options) + transformer = Transformer(result=runner_result, options=config_options) + transformer.run() + + matches = runner_result.matches + assert len(matches) == 3 + + orig_content = pattern_file.read_text(encoding="utf-8") + transformed_content = pattern_file.with_suffix( + f".tmp{pattern_file.suffix}" + ).read_text( + encoding="utf-8", + ) + + assert orig_content != transformed_content + transformed_name = json.loads(transformed_content)["name"] + assert transformed_name == "transform_pattern" + pattern_file.with_suffix(f".tmp{pattern_file.suffix}").unlink()