Repository: ansible-collections/community.general

Issue:
yarn: #5829 causes "global dir" and "global: GTAGS not found." error
### Summary

I am doing `yarn global add ...` in ansible-core version 2.14.3 and experiencing this error that doesn't happen in the previous 2.14.2 version. I have investigated that the cause is from the pull request #5829

```
TASK [install_software : Install graph cli] ************************************
fatal: [node]: FAILED! => {"changed": false, "cmd": "global dir", "msg": "global: GTAGS not found.", "rc": 3, "stderr": "global: GTAGS not found.\n", "stderr_lines": ["global: GTAGS not found."], "stdout": "", "stdout_lines": []}
```

That is because it can't detect yarn as an executable in this line of code.

https://github.com/ansible-collections/community.general/pull/5829/files#diff-baebe07da57aab76abf01ac7c4b8d6730b6111f18813e93a898a217c752568d3R343

So it runs `global dir` instead of the expected `yarn global dir` command. 

### Issue Type

Bug Report

### Component Name

yarn

### Ansible Version

```console (paste below)
$ ansible --version

ansible [core 2.14.3]
  config file = None
  configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/local/lib/python3.10/dist-packages/ansible
  ansible collection location = /root/.ansible/collections:/usr/share/ansible/collections
  executable location = /usr/local/bin/ansible
  python version = 3.10.6 (main, Nov 14 2022, 16:10:14) [GCC 11.3.0] (/usr/bin/python3)
  jinja version = 3.1.2
  libyaml = False
```


### Community.general Version

```console (paste below)
$ ansible-galaxy collection list community.general

# /usr/local/lib/python3.10/dist-packages/ansible_collections
Collection        Version
----------------- -------
community.general 6.4.0
```


### Configuration

```console (paste below)
$ ansible-config dump --only-changed

CONFIG_FILE() = None
HOST_KEY_CHECKING(env: ANSIBLE_HOST_KEY_CHECKING) = False
```


### OS / Environment

A docker container running Ubuntu 22.04 image

### Steps to Reproduce

<!--- Paste example playbooks or commands between quotes below -->
```yaml (paste below)
- name: "Install graph cli"
  yarn:
    name: '@graphprotocol/graph-cli'
    version: '0.42.3'
    global: true
```


### Expected Results

It should run `yarn global dir`

### Actual Results

It runs "global dir" instead (missing yarn)

```console (paste below)
TASK [install_software : Install graph cli] ************************************
fatal: [node]: FAILED! => {"changed": false, "cmd": "global dir", "msg": "global: GTAGS not found.", "rc": 3, "stderr": "global: GTAGS not found.\n", "stderr_lines": ["global: GTAGS not found."], "stdout": "", "stdout_lines": []}
```


### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:
Files identified in the description:
* [`plugins/modules/yarn.py`](https://github.com/ansible-collections/community.general/blob/main/plugins/modules/yarn.py)

If these files are incorrect, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @chrishoffman @verkaufer
[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: notify --->
CC @sargunv @russoz
Ah I see, this line executes before the path to Yarn is identified from the PATH if it wasn't passed explicitly:

```py
module.run_command([executable, 'global', 'dir'], check_rc=True)
```


Pulling this logic out of the initializer so it runs before the above line should fix it:

```py
if kwargs['executable']:
    self.executable = kwargs['executable'].split(' ')
else:
    self.executable = [module.get_bin_path('yarn', True)]
```
And since the tests all explicitly set the `executable`, this wasn't caught:

```yml
    - name: 'Global install binary with explicit version (older version of package)'
      yarn:
        global: true
        executable: '{{ yarn_bin_path }}/yarn'  # THIS LINE
        name: prettier
        version: 2.0.0
        state: present
      environment:
        PATH: '{{ node_bin_path }}:{{ ansible_env.PATH }}'
      register: yarn_global_install_old_binary
```

Will file a PR soon, but a workaround to get y'all unblocked might work something like this:

```yml
    - name: example
      yarn:
        global: true
        executable: "{{ lookup('lines', 'which yarn') }}"  # THIS LINE
        name: prettier
        version: 2.0.0
        state: present
```

Resolving patch:
diff --git a/plugins/modules/yarn.py b/plugins/modules/yarn.py
--- a/plugins/modules/yarn.py
+++ b/plugins/modules/yarn.py
@@ -179,15 +179,11 @@ def __init__(self, module, **kwargs):
         self.registry = kwargs['registry']
         self.production = kwargs['production']
         self.ignore_scripts = kwargs['ignore_scripts']
+        self.executable = kwargs['executable']
 
         # Specify a version of package if version arg passed in
         self.name_version = None
 
-        if kwargs['executable']:
-            self.executable = kwargs['executable'].split(' ')
-        else:
-            self.executable = [module.get_bin_path('yarn', True)]
-
         if kwargs['version'] and self.name is not None:
             self.name_version = self.name + '@' + str(self.version)
         elif self.name is not None:
@@ -328,7 +324,6 @@ def main():
     version = module.params['version']
     globally = module.params['global']
     production = module.params['production']
-    executable = module.params['executable']
     registry = module.params['registry']
     state = module.params['state']
     ignore_scripts = module.params['ignore_scripts']
@@ -345,9 +340,14 @@ def main():
     if state == 'latest':
         version = 'latest'
 
+    if module.params['executable']:
+        executable = module.params['executable'].split(' ')
+    else:
+        executable = [module.get_bin_path('yarn', True)]
+
     # When installing globally, use the defined path for global node_modules
     if globally:
-        _rc, out, _err = module.run_command([executable, 'global', 'dir'], check_rc=True)
+        _rc, out, _err = module.run_command(executable + ['global', 'dir'], check_rc=True)
         path = out.strip()
 
     yarn = Yarn(module,


Test patch:
diff --git a/tests/integration/targets/yarn/tasks/run.yml b/tests/integration/targets/yarn/tasks/run.yml
--- a/tests/integration/targets/yarn/tasks/run.yml
+++ b/tests/integration/targets/yarn/tasks/run.yml
@@ -108,6 +108,15 @@
         that:
           - yarn_install_old_package is changed
 
+    - name: 'Again but without explicit executable path'
+      yarn:
+        path: '{{ remote_tmp_dir }}'
+        name: left-pad
+        version: 1.1.0
+        state: present
+      environment:
+        PATH: '{{ yarn_bin_path }}:{{ node_bin_path }}:{{ ansible_env.PATH }}'
+
     - name: 'Upgrade old package'
       yarn:
         path: '{{ remote_tmp_dir }}'