Repository: ansible-collections/community.general

Issue:
Composer 'require' command is not idempotent with Composer v2
##### SUMMARY

The `require` command for the `composer` module seems to no longer be idempotent. If I require one or more packages using the `composer` module, the module always reports 'changed', thus breaking idempotence in my playbooks.

It does seem to still work, installing the required package, but it just always reports a change.

##### ISSUE TYPE
- Bug Report

##### COMPONENT NAME

community.general.composer

##### ANSIBLE VERSION

```
# ansible --version
ansible 2.10.1
  config file = None
  configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/local/lib/python3.6/dist-packages/ansible
  executable location = /usr/local/bin/ansible
  python version = 3.6.9 (default, Jul 17 2020, 12:50:27) [GCC 8.4.0]
```

##### CONFIGURATION

```
N/A
```

##### OS / ENVIRONMENT

Linux (CentOS, Ubuntu) and macOS (10.15)

##### STEPS TO REPRODUCE

```yaml
- name: Install dependencies with composer require.
  composer:
    command: require
    arguments: "drush/drush:^10.1"
    working_dir: "/var/www/drupal"
  become: false
  environment:
    COMPOSER_PROCESS_TIMEOUT: 1200
    COMPOSER_MEMORY_LIMIT: '-1'
```

##### EXPECTED RESULTS

If running that task two times in a row, the second run would report `ok` with no changes.

##### ACTUAL RESULTS

The second time the task runs, it still reports a change:

```
    TASK [geerlingguy.drupal : Install dependencies with composer require (this may take a while).] ***
    task path: /Users/jgeerling/Dropbox/VMs/roles/geerlingguy.drupal/tasks/build-composer-project.yml:36
    redirecting (type: connection) ansible.builtin.docker to community.general.docker
    <instance> ESTABLISH DOCKER CONNECTION FOR USER: root
    <instance> EXEC ['/usr/local/bin/docker', b'exec', b'-i', 'instance', '/bin/sh', '-c', "/bin/sh -c 'echo ~ && sleep 0'"]
    <instance> EXEC ['/usr/local/bin/docker', b'exec', b'-i', 'instance', '/bin/sh', '-c', '/bin/sh -c \'( umask 77 && mkdir -p "` echo /root/.ansible/tmp `"&& mkdir "` echo /root/.ansible/tmp/ansible-tmp-1603724927.233592-2797-186180795085662 `" && echo ansible-tmp-1603724927.233592-2797-186180795085662="` echo /root/.ansible/tmp/ansible-tmp-1603724927.233592-2797-186180795085662 `" ) && sleep 0\'']
    redirecting (type: modules) ansible.builtin.composer to community.general.composer
    Using module file /Users/jgeerling/.ansible/collections/ansible_collections/community/general/plugins/modules/composer.py
    <instance> PUT /Users/jgeerling/.ansible/tmp/ansible-local-99444fg58b7oc/tmpwxfpvg5d TO /root/.ansible/tmp/ansible-tmp-1603724927.233592-2797-186180795085662/AnsiballZ_composer.py
    <instance> EXEC ['/usr/local/bin/docker', b'exec', b'-i', 'instance', '/bin/sh', '-c', "/bin/sh -c 'chmod u+x /root/.ansible/tmp/ansible-tmp-1603724927.233592-2797-186180795085662/ /root/.ansible/tmp/ansible-tmp-1603724927.233592-2797-186180795085662/AnsiballZ_composer.py && sleep 0'"]
    <instance> EXEC ['/usr/local/bin/docker', b'exec', b'-i', 'instance', '/bin/sh', '-c', "/bin/sh -c 'COMPOSER_MEMORY_LIMIT=-1 COMPOSER_PROCESS_TIMEOUT=1200 /usr/bin/python3 /root/.ansible/tmp/ansible-tmp-1603724927.233592-2797-186180795085662/AnsiballZ_composer.py && sleep 0'"]
    <instance> EXEC ['/usr/local/bin/docker', b'exec', b'-i', 'instance', '/bin/sh', '-c', "/bin/sh -c 'rm -f -r /root/.ansible/tmp/ansible-tmp-1603724927.233592-2797-186180795085662/ > /dev/null 2>&1 && sleep 0'"]
    changed: [instance] => (item=drush/drush:^10.1) => {
        "ansible_loop_var": "item",
        "changed": true,
        "invocation": {
            "module_args": {
                "apcu_autoloader": false,
                "arguments": "drush/drush:^10.1",
                "classmap_authoritative": false,
                "command": "require",
                "executable": null,
                "global_command": false,
                "ignore_platform_reqs": false,
                "no_dev": true,
                "no_plugins": false,
                "no_scripts": false,
                "optimize_autoloader": true,
                "prefer_dist": false,
                "prefer_source": false,
                "working_dir": "/var/www/drupal"
            }
        },
        "item": "drush/drush:^10.1",
        "msg": "./composer.json has been updated Running composer update drush/drush Loading composer repositories with package information Updating dependencies Nothing to modify in lock file Installing dependencies from lock file (including require-dev) Nothing to install, update or remove Package container-interop/container-interop is abandoned, you should avoid using it. Use psr/container instead. Package phpunit/php-token-stream is abandoned, you should avoid using it. No replacement was suggested. Generating optimized autoload files 54 packages you are using are looking for funding. Use the `composer fund` command to find out more!",
        "stdout": "./composer.json has been updated\nRunning composer update drush/drush\nLoading composer repositories with package information\nUpdating dependencies\nNothing to modify in lock file\nInstalling dependencies from lock file (including require-dev)\nNothing to install, update or remove\nPackage container-interop/container-interop is abandoned, you should avoid using it. Use psr/container instead.\nPackage phpunit/php-token-stream is abandoned, you should avoid using it. No replacement was suggested.\nGenerating optimized autoload files\n54 packages you are using are looking for funding.\nUse the `composer fund` command to find out more!\n",
        "stdout_lines": [
            "./composer.json has been updated",
            "Running composer update drush/drush",
            "Loading composer repositories with package information",
            "Updating dependencies",
            "Nothing to modify in lock file",
            "Installing dependencies from lock file (including require-dev)",
            "Nothing to install, update or remove",
            "Package container-interop/container-interop is abandoned, you should avoid using it. Use psr/container instead.",
            "Package phpunit/php-token-stream is abandoned, you should avoid using it. No replacement was suggested.",
            "Generating optimized autoload files",
            "54 packages you are using are looking for funding.",
            "Use the `composer fund` command to find out more!"
        ]
    }
```

Related: https://github.com/geerlingguy/ansible-role-drupal/issues/83


Discussion:
Just noting, for reference, the difference in stdout between the same `composer require` command in:

Version 1.10.16:

```
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Nothing to install or update
Package container-interop/container-interop is abandoned, you should avoid using it. Use psr/container instead.
Package phpunit/php-token-stream is abandoned, you should avoid using it. No replacement was suggested.
Writing lock file
Generating autoload files
54 packages you are using are looking for funding.
Use the `composer fund` command to find out more!
```

(and an aside: under v1 I got a PHP fatal error from the memory limit, so had to add `COMPOSER_MEMORY_LIMIT=-1`)

Version 2.0.2:

```
./composer.json has been updated
Running composer update drush/drush
Loading composer repositories with package information
Updating dependencies
Nothing to modify in lock file
Installing dependencies from lock file (including require-dev)
Nothing to install, update or remove
Package container-interop/container-interop is abandoned, you should avoid using it. Use psr/container instead.
Package phpunit/php-token-stream is abandoned, you should avoid using it. No replacement was suggested.
Generating autoload files
54 packages you are using are looking for funding.
Use the `composer fund` command to find out more!
```
It looks like the problem is the following `has_changed` check needs updating to account for the differnce in v2:

```python
def has_changed(string):
    return "Nothing to install or update" not in string
```

In v2, the string is now:

```
Nothing to install, update or remove
```

(It also includes another line `Nothing to modify in lock file`).

Resolving patch:
diff --git a/plugins/modules/packaging/language/composer.py b/plugins/modules/packaging/language/composer.py
--- a/plugins/modules/packaging/language/composer.py
+++ b/plugins/modules/packaging/language/composer.py
@@ -145,7 +145,11 @@ def parse_out(string):
 
 
 def has_changed(string):
-    return "Nothing to install or update" not in string
+    for no_change in ["Nothing to install or update", "Nothing to install, update or remove"]:
+        if no_change in string:
+            return False
+
+    return True
 
 
 def get_available_options(module, command='install'):


Test patch: