diff --git a/changelogs/fragments/11689-composer-create-project-idempotent.yml b/changelogs/fragments/11689-composer-create-project-idempotent.yml new file mode 100644 index 0000000000..6235e3e878 --- /dev/null +++ b/changelogs/fragments/11689-composer-create-project-idempotent.yml @@ -0,0 +1,6 @@ +minor_changes: + - composer - add ``force`` parameter; when ``command=create-project``, the module now checks + whether a ``composer.json`` already exists in ``working_dir`` and skips the command if so, + making the task idempotent. Set ``force=true`` to always run the command regardless + (https://github.com/ansible-collections/community.general/issues/725, + https://github.com/ansible-collections/community.general/pull/11689). diff --git a/plugins/modules/composer.py b/plugins/modules/composer.py index c649aae37f..731cfa1250 100644 --- a/plugins/modules/composer.py +++ b/plugins/modules/composer.py @@ -98,6 +98,14 @@ options: these. default: false type: bool + force: + description: + - When O(command) is V(create-project), the module checks whether a V(composer.json) already + exists in O(working_dir) and skips the command if it does, making the task idempotent. + - Set to V(true) to always run the command regardless. + default: false + type: bool + version_added: 12.6.0 composer_executable: type: path description: @@ -139,6 +147,7 @@ EXAMPLES = r""" arguments: my/package """ +import os import re import shlex @@ -212,6 +221,7 @@ def main(): optimize_autoloader=dict(default=True, type="bool"), classmap_authoritative=dict(default=False, type="bool"), ignore_platform_reqs=dict(default=False, type="bool"), + force=dict(default=False, type="bool"), composer_executable=dict(type="path"), ), required_if=[("global_command", False, ["working_dir"])], @@ -257,6 +267,12 @@ def main(): option = f"--{option}" options.append(option) + working_dir = module.params["working_dir"] + + if command == "create-project" and not module.params["force"]: + if working_dir and os.path.exists(os.path.join(working_dir, "composer.json")): + module.exit_json(changed=False, msg="composer.json already exists in working_dir, skipping create-project") + if module.check_mode: if "dry-run" in available_options: options.append("--dry-run") diff --git a/tests/unit/plugins/modules/test_composer.py b/tests/unit/plugins/modules/test_composer.py index 71648bfa86..b0ea50a47d 100644 --- a/tests/unit/plugins/modules/test_composer.py +++ b/tests/unit/plugins/modules/test_composer.py @@ -9,6 +9,17 @@ from __future__ import annotations from ansible_collections.community.general.plugins.modules import composer -from .uthelper import RunCommandMock, UTHelper +from .uthelper import RunCommandMock, TestCaseMock, UTHelper -UTHelper.from_module(composer, __name__, mocks=[RunCommandMock]) + +class OsPathExistsMock(TestCaseMock): + name = "os_path_exists" + + def setup(self, mocker): + mocker.patch("os.path.exists", return_value=self.mock_specs.get("return_value", False)) + + def check(self, test_case, results): + pass + + +UTHelper.from_module(composer, __name__, mocks=[RunCommandMock, OsPathExistsMock]) diff --git a/tests/unit/plugins/modules/test_composer.yaml b/tests/unit/plugins/modules/test_composer.yaml index 545e9d0176..323f1b2516 100644 --- a/tests/unit/plugins/modules/test_composer.yaml +++ b/tests/unit/plugins/modules/test_composer.yaml @@ -3,6 +3,25 @@ # SPDX-License-Identifier: GPL-3.0-or-later --- +anchors: + help_install: &help_install + command: ["/testbin/php", "/testbin/composer", "help", "install", "--working-dir", "/var/www/foo", "--no-interaction", "--format=json"] + rc: 0 + out: | + {"definition": {"options": ["a", "b", "c"]}} + err: '' + help_create_project: &help_create_project + command: ["/testbin/php", "/testbin/composer", "help", "create-project", "--working-dir", "/var/www/foo", "--no-interaction", "--format=json"] + rc: 0 + out: | + {"definition": {"options": ["a", "b", "c"]}} + err: '' + run_create_project: &run_create_project + command: ["/testbin/php", "/testbin/composer", "create-project", "--working-dir", "/var/www/foo", "vendor/package"] + rc: 0 + out: '' + err: '' + test_cases: - id: composer input: @@ -12,12 +31,50 @@ test_cases: changed: true mocks: run_command: - - command: ["/testbin/php", "/testbin/composer", "help", "install", "--working-dir", "/var/www/foo", "--no-interaction", "--format=json"] - rc: 0 - out: | - {"definition": {"options": ["a", "b", "c"]}} - err: '' + - *help_install - command: ["/testbin/php", "/testbin/composer", "install", "--working-dir", "/var/www/foo"] rc: 0 out: '' err: '' + + - id: create_project_runs + input: + command: create-project + arguments: "vendor/package" + working_dir: "/var/www/foo" + output: + changed: true + mocks: + os_path_exists: + return_value: false + run_command: + - *help_create_project + - *run_create_project + + - id: create_project_skips_when_exists + input: + command: create-project + arguments: "vendor/package" + working_dir: "/var/www/foo" + output: + changed: false + mocks: + os_path_exists: + return_value: true + run_command: + - *help_create_project + + - id: create_project_force + input: + command: create-project + arguments: "vendor/package" + working_dir: "/var/www/foo" + force: true + output: + changed: true + mocks: + os_path_exists: + return_value: true + run_command: + - *help_create_project + - *run_create_project