From 2fa17c32a3b08d2065f0b6e97edddaab38a5f236 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Fri, 8 Sep 2023 17:38:30 +0200 Subject: [PATCH] [PR #6741/568814fc backport][stable-7] New module: pnpm package manager (#7225) New module: pnpm package manager (#6741) * (feat) New module pnpm added A new module for pnpm is added. Necessary entries were written to BOTMETA.yml. * (feat) Basic tests added * (feat) reduced nesting of ifs * (fix) trying to fix up CI pipelines * (fix) incorrect indentation of alias fixed * (feat) fixed further indentations * Apply suggestions from code review Co-authored-by: Felix Fontein * (fix) various linting and CI errors fixed/ignored * (feat) reduced restriction, new install method Some restrictions on OS are reduced. New installation method, similar to the official installation method, is provided. * (fix) ignoring CentOS 6 for CI. * retrigger checks --------- Co-authored-by: Felix Fontein (cherry picked from commit 568814fc3e1f63e673b4146e42af87b2f9ca9824) Co-authored-by: Aritra Sen <125266845+aretrosen@users.noreply.github.com> --- .github/BOTMETA.yml | 3 + plugins/modules/pnpm.py | 463 ++++++++++++++++++ tests/integration/targets/pnpm/aliases | 8 + tests/integration/targets/pnpm/meta/main.yml | 9 + tests/integration/targets/pnpm/tasks/main.yml | 26 + tests/integration/targets/pnpm/tasks/run.yml | 311 ++++++++++++ .../targets/pnpm/templates/package.j2 | 13 + 7 files changed, 833 insertions(+) create mode 100644 plugins/modules/pnpm.py create mode 100644 tests/integration/targets/pnpm/aliases create mode 100644 tests/integration/targets/pnpm/meta/main.yml create mode 100644 tests/integration/targets/pnpm/tasks/main.yml create mode 100644 tests/integration/targets/pnpm/tasks/run.yml create mode 100644 tests/integration/targets/pnpm/templates/package.j2 diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index b53a01a9db..67ff467105 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -980,6 +980,9 @@ files: maintainers: $team_solaris dermute $modules/pmem.py: maintainers: mizumm + $modules/pnpm.py: + ignore: chrishoffman + maintainers: aretrosen $modules/portage.py: ignore: sayap labels: portage diff --git a/plugins/modules/pnpm.py b/plugins/modules/pnpm.py new file mode 100644 index 0000000000..8889ffc15b --- /dev/null +++ b/plugins/modules/pnpm.py @@ -0,0 +1,463 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2023 Aritra Sen +# Copyright (c) 2017 Chris Hoffman +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +DOCUMENTATION = """ +--- +module: pnpm +short_description: Manage node.js packages with pnpm +version_added: 7.4.0 +description: + - Manage node.js packages with the L(pnpm package manager, https://pnpm.io/). +author: + - "Aritra Sen (@aretrosen)" + - "Chris Hoffman (@chrishoffman), creator of NPM Ansible module" +extends_documentation_fragment: + - community.general.attributes +attributes: + check_mode: + support: full + diff_mode: + support: none +options: + name: + description: + - The name of a node.js library to install. + - All packages in package.json are installed if not provided. + type: str + required: false + alias: + description: + - Alias of the node.js library. + type: str + required: false + path: + description: + - The base path to install the node.js libraries. + type: path + required: false + version: + description: + - The version of the library to be installed, in semver format. + type: str + required: false + global: + description: + - Install the node.js library globally. + required: false + default: false + type: bool + executable: + description: + - The executable location for pnpm. + - The default location it searches for is E(PATH), fails if not set. + type: path + required: false + ignore_scripts: + description: + - Use the C(--ignore-scripts) flag when installing. + required: false + type: bool + default: false + no_optional: + description: + - Do not install optional packages, equivalent to C(--no-optional). + required: false + type: bool + default: false + production: + description: + - Install dependencies in production mode. + - Pnpm will ignore any dependencies under C(devDependencies) in package.json. + required: false + type: bool + default: false + dev: + description: + - Install dependencies in development mode. + - Pnpm will ignore any regular dependencies in C(package.json). + required: false + default: false + type: bool + optional: + description: + - Install dependencies in optional mode. + required: false + default: false + type: bool + state: + description: + - Installation state of the named node.js library. + - If V(absent) is selected, a name option must be provided. + type: str + required: false + default: present + choices: ["present", "absent", "latest"] +requirements: + - Pnpm executable present in E(PATH). +""" + +EXAMPLES = """ +- name: Install "tailwindcss" node.js package. + community.general.pnpm: + name: tailwindcss + path: /app/location + +- name: Install "tailwindcss" node.js package on version 3.3.2 + community.general.pnpm: + name: tailwindcss + version: 3.3.2 + path: /app/location + +- name: Install "tailwindcss" node.js package globally. + community.general.pnpm: + name: tailwindcss + global: true + +- name: Install "tailwindcss" node.js package as dev dependency. + community.general.pnpm: + name: tailwindcss + path: /app/location + dev: true + +- name: Install "tailwindcss" node.js package as optional dependency. + community.general.pnpm: + name: tailwindcss + path: /app/location + optional: true + +- name: Install "tailwindcss" node.js package version 0.1.3 as tailwind-1 + community.general.pnpm: + name: tailwindcss + alias: tailwind-1 + version: 0.1.3 + path: /app/location + +- name: Remove the globally-installed package "tailwindcss". + community.general.pnpm: + name: tailwindcss + global: true + state: absent + +- name: Install packages based on package.json. + community.general.pnpm: + path: /app/location + +- name: Update all packages in package.json to their latest version. + community.general.pnpm: + path: /app/location + state: latest +""" +import json +import os + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + + +class Pnpm(object): + def __init__(self, module, **kwargs): + self.module = module + self.name = kwargs["name"] + self.alias = kwargs["alias"] + self.version = kwargs["version"] + self.path = kwargs["path"] + self.globally = kwargs["globally"] + self.executable = kwargs["executable"] + self.ignore_scripts = kwargs["ignore_scripts"] + self.no_optional = kwargs["no_optional"] + self.production = kwargs["production"] + self.dev = kwargs["dev"] + self.optional = kwargs["optional"] + + self.alias_name_ver = None + + if self.alias is not None: + self.alias_name_ver = self.alias + "@npm:" + + if self.name is not None: + self.alias_name_ver = (self.alias_name_ver or "") + self.name + if self.version is not None: + self.alias_name_ver = self.alias_name_ver + "@" + str(self.version) + + def _exec(self, args, run_in_check_mode=False, check_rc=True): + if not self.module.check_mode or (self.module.check_mode and run_in_check_mode): + cmd = self.executable + args + + if self.globally: + cmd.append("-g") + + if self.ignore_scripts: + cmd.append("--ignore-scripts") + + if self.no_optional: + cmd.append("--no-optional") + + if self.production: + cmd.append("-P") + + if self.dev: + cmd.append("-D") + + if self.name and self.optional: + cmd.append("-O") + + # If path is specified, cd into that path and run the command. + cwd = None + if self.path: + if not os.path.exists(self.path): + os.makedirs(self.path) + + if not os.path.isdir(self.path): + self.module.fail_json(msg="Path %s is not a directory" % self.path) + + if not self.alias_name_ver and not os.path.isfile( + os.path.join(self.path, "package.json") + ): + self.module.fail_json( + msg="package.json does not exist in provided path" + ) + + cwd = self.path + + _rc, out, err = self.module.run_command(cmd, check_rc=check_rc, cwd=cwd) + return out, err + + return None, None + + def missing(self): + if not os.path.isfile(os.path.join(self.path, "pnpm-lock.yaml")): + return True + + cmd = ["list", "--json"] + + if self.name is not None: + cmd.append(self.name) + + try: + out, err = self._exec(cmd, True, False) + if err is not None and err != "": + raise Exception(out) + + data = json.loads(out) + except Exception as e: + self.module.fail_json( + msg="Failed to parse pnpm output with error %s" % to_native(e) + ) + + if "error" in data: + return True + + data = data[0] + + for typedep in [ + "dependencies", + "devDependencies", + "optionalDependencies", + "unsavedDependencies", + ]: + if typedep not in data: + continue + + for dep, prop in data[typedep].items(): + if self.alias is not None and self.alias != dep: + continue + + name = prop["from"] if self.alias is not None else dep + if self.name != name: + continue + + if self.version is None or self.version == prop["version"]: + return False + + break + + return True + + def install(self): + if self.alias_name_ver is not None: + return self._exec(["add", self.alias_name_ver]) + return self._exec(["install"]) + + def update(self): + return self._exec(["update", "--latest"]) + + def uninstall(self): + if self.alias is not None: + return self._exec(["remove", self.alias]) + return self._exec(["remove", self.name]) + + def list_outdated(self): + if not os.path.isfile(os.path.join(self.path, "pnpm-lock.yaml")): + return list() + + cmd = ["outdated", "--format", "json"] + try: + out, err = self._exec(cmd, True, False) + + # BUG: It will not show correct error sometimes, like when it has + # plain text output intermingled with a {} + if err is not None and err != "": + raise Exception(out) + + # HACK: To fix the above bug, the following hack is implemented + data_lines = out.splitlines(True) + + out = None + for line in data_lines: + if len(line) > 0 and line[0] == "{": + out = line + continue + + if len(line) > 0 and line[0] == "}": + out += line + break + + if out is not None: + out += line + + data = json.loads(out) + except Exception as e: + self.module.fail_json( + msg="Failed to parse pnpm output with error %s" % to_native(e) + ) + + return data.keys() + + +def main(): + arg_spec = dict( + name=dict(default=None), + alias=dict(default=None), + path=dict(default=None, type="path"), + version=dict(default=None), + executable=dict(default=None, type="path"), + ignore_scripts=dict(default=False, type="bool"), + no_optional=dict(default=False, type="bool"), + production=dict(default=False, type="bool"), + dev=dict(default=False, type="bool"), + optional=dict(default=False, type="bool"), + state=dict(default="present", choices=["present", "absent", "latest"]), + ) + arg_spec["global"] = dict(default=False, type="bool") + module = AnsibleModule(argument_spec=arg_spec, supports_check_mode=True) + + name = module.params["name"] + alias = module.params["alias"] + path = module.params["path"] + version = module.params["version"] + globally = module.params["global"] + ignore_scripts = module.params["ignore_scripts"] + no_optional = module.params["no_optional"] + production = module.params["production"] + dev = module.params["dev"] + optional = module.params["optional"] + state = module.params["state"] + + if module.params["executable"]: + executable = module.params["executable"].split(" ") + else: + executable = [module.get_bin_path("pnpm", True)] + + if name is None and version is not None: + module.fail_json(msg="version is meaningless when name is not provided") + + if name is None and alias is not None: + module.fail_json(msg="alias is meaningless when name is not provided") + + if path is None and not globally: + module.fail_json(msg="path must be specified when not using global") + elif path is not None and globally: + module.fail_json(msg="Cannot specify path when doing global installation") + + if globally and (production or dev or optional): + module.fail_json( + msg="Options production, dev, and optional is meaningless when installing packages globally" + ) + + if name is not None and path is not None and globally: + module.fail_json(msg="path should not be mentioned when installing globally") + + if production and dev and optional: + module.fail_json( + msg="Options production and dev and optional don't go together" + ) + + if production and dev: + module.fail_json(msg="Options production and dev don't go together") + + if production and optional: + module.fail_json(msg="Options production and optional don't go together") + + if dev and optional: + module.fail_json(msg="Options dev and optional don't go together") + + if name is not None and name[0:4] == "http" and version is not None: + module.fail_json(msg="Semver not supported on remote url downloads") + + if name is None and optional: + module.fail_json( + msg="Optional not available when package name not provided, use no_optional instead" + ) + + if state == "absent" and name is None: + module.fail_json(msg="Package name is required for uninstalling") + + if state == "latest": + version = "latest" + + if globally: + _rc, out, _err = module.run_command(executable + ["root", "-g"], check_rc=True) + path, _tail = os.path.split(out.strip()) + + pnpm = Pnpm( + module, + name=name, + alias=alias, + path=path, + version=version, + globally=globally, + executable=executable, + ignore_scripts=ignore_scripts, + no_optional=no_optional, + production=production, + dev=dev, + optional=optional, + ) + + changed = False + out = "" + err = "" + if state == "present": + if pnpm.missing(): + changed = True + out, err = pnpm.install() + elif state == "latest": + outdated = pnpm.list_outdated() + if name is not None: + if pnpm.missing() or name in outdated: + changed = True + out, err = pnpm.install() + elif len(outdated): + changed = True + out, err = pnpm.update() + else: # absent + if not pnpm.missing(): + changed = True + out, err = pnpm.uninstall() + + module.exit_json(changed=changed, out=out, err=err) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/pnpm/aliases b/tests/integration/targets/pnpm/aliases new file mode 100644 index 0000000000..afeb1a6ee5 --- /dev/null +++ b/tests/integration/targets/pnpm/aliases @@ -0,0 +1,8 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/posix/2 +destructive +skip/macos +skip/freebsd diff --git a/tests/integration/targets/pnpm/meta/main.yml b/tests/integration/targets/pnpm/meta/main.yml new file mode 100644 index 0000000000..6147ad33e5 --- /dev/null +++ b/tests/integration/targets/pnpm/meta/main.yml @@ -0,0 +1,9 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_pkg_mgr + - setup_gnutar + - setup_remote_tmp_dir diff --git a/tests/integration/targets/pnpm/tasks/main.yml b/tests/integration/targets/pnpm/tasks/main.yml new file mode 100644 index 0000000000..ae48f994a2 --- /dev/null +++ b/tests/integration/targets/pnpm/tasks/main.yml @@ -0,0 +1,26 @@ +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +# test code for the pnpm module +# Copyright (c) 2023 Aritra Sen +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# ------------------------------------------------------------- +# Setup steps + +- name: Run tests on OSes + include_tasks: run.yml + vars: + ansible_system_os: "{{ ansible_system | lower }}" + nodejs_version: "{{ item.node_version }}" + nodejs_path: "node-v{{ nodejs_version }}-{{ ansible_system_os }}-x{{ ansible_userspace_bits }}" + pnpm_version: "{{ item.pnpm_version }}" + pnpm_path: "pnpm-{{ 'macos' if ansible_system_os == 'darwin' else 'linuxstatic' }}-x{{ ansible_userspace_bits }}" + with_items: + - { node_version: 16.20.0, pnpm_version: 8.7.0 } + when: + - not(ansible_distribution == 'Alpine') and not(ansible_distribution == 'CentOS' and ansible_distribution_major_version == '6') diff --git a/tests/integration/targets/pnpm/tasks/run.yml b/tests/integration/targets/pnpm/tasks/run.yml new file mode 100644 index 0000000000..0b0f49e629 --- /dev/null +++ b/tests/integration/targets/pnpm/tasks/run.yml @@ -0,0 +1,311 @@ +--- +# Copyright (c) 2023 Aritra Sen +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Download nodejs + ansible.builtin.unarchive: + src: "https://nodejs.org/dist/v{{ nodejs_version }}/{{ nodejs_path }}.tar.gz" + dest: "{{ remote_tmp_dir }}" + remote_src: true + creates: "{{ remote_tmp_dir }}/{{ nodejs_path }}.tar.gz" + +- name: Create a temporary directory for pnpm binary + ansible.builtin.tempfile: + state: directory + register: tmp_dir + +- name: Download pnpm binary to the temporary directory + ansible.builtin.get_url: + url: "https://github.com/pnpm/pnpm/releases/download/v{{ pnpm_version }}/{{ pnpm_path }}" + dest: "{{ tmp_dir.path }}/pnpm" + mode: "755" + +- name: Setting up pnpm via command + ansible.builtin.command: "{{ tmp_dir.path }}/pnpm setup --force" + environment: + PNPM_HOME: "{{ ansible_env.HOME }}/.local/share/pnpm" + SHELL: /bin/sh + ENV: "{{ ansible_env.HOME }}/.shrc" + +- name: Remove the temporary directory + ansible.builtin.file: + path: "{{ tmp_dir.path }}" + state: absent + +- name: Remove any previous Nodejs modules + ansible.builtin.file: + path: "{{ remote_tmp_dir }}/node_modules" + state: absent + +- name: CI tests to run + vars: + node_bin_path: "{{ remote_tmp_dir }}/{{ nodejs_path }}/bin" + pnpm_bin_path: "{{ ansible_env.HOME }}/.local/share/pnpm" + package: "tailwindcss" + environment: + PATH: "{{ node_bin_path }}:{{ ansible_env.PATH }}" + + block: + - name: Create dummy package.json + ansible.builtin.template: + src: package.j2 + dest: "{{ remote_tmp_dir }}/package.json" + mode: "644" + + - name: Install reading-time package via package.json + pnpm: + path: "{{ remote_tmp_dir }}" + executable: "{{ pnpm_bin_path }}/pnpm" + state: present + environment: + PATH: "{{ node_bin_path }}:{{ ansible_env.PATH }}" + + - name: Install the same package from package.json again + pnpm: + path: "{{ remote_tmp_dir }}" + executable: "{{ pnpm_bin_path }}/pnpm" + name: "reading-time" + state: present + environment: + PATH: "{{ node_bin_path }}:{{ ansible_env.PATH }}" + register: pnpm_install + + - name: Assert that result is not changed + ansible.builtin.assert: + that: + - not (pnpm_install is changed) + + - name: Install all packages in check mode + pnpm: + path: "{{ remote_tmp_dir }}" + executable: "{{ pnpm_bin_path }}/pnpm" + state: present + environment: + PATH: "{{ node_bin_path }}:{{ ansible_env.PATH }}" + check_mode: true + register: pnpm_install_check + + - name: Verify test pnpm global installation in check mode + ansible.builtin.assert: + that: + - pnpm_install_check.err is defined + - pnpm_install_check.out is defined + - pnpm_install_check.err is none + - pnpm_install_check.out is none + + - name: Install package without dependency + pnpm: + path: "{{ remote_tmp_dir }}" + executable: "{{ pnpm_bin_path }}/pnpm" + state: present + name: "{{ package }}" + environment: + PATH: "{{ node_bin_path }}:{{ ansible_env.PATH }}" + register: pnpm_install + + - name: Assert that result is changed and successful + ansible.builtin.assert: + that: + - pnpm_install is success + - pnpm_install is changed + + - name: Reinstall package without dependency + pnpm: + path: "{{ remote_tmp_dir }}" + executable: "{{ pnpm_bin_path }}/pnpm" + state: present + name: "{{ package }}" + environment: + PATH: "{{ node_bin_path }}:{{ ansible_env.PATH }}" + register: pnpm_reinstall + + - name: Assert that there is no change + ansible.builtin.assert: + that: + - pnpm_reinstall is success + - not (pnpm_reinstall is changed) + + - name: Manually delete package + ansible.builtin.file: + path: "{{ remote_tmp_dir }}/node_modules/{{ package }}" + state: absent + + - name: Reinstall package + pnpm: + path: "{{ remote_tmp_dir }}" + executable: "{{ pnpm_bin_path }}/pnpm" + state: latest + name: "{{ package }}" + environment: + PATH: "{{ node_bin_path }}:{{ ansible_env.PATH }}" + register: pnpm_fix_install + + - name: Assert that result is changed and successful + ansible.builtin.assert: + that: + - pnpm_fix_install is success + - pnpm_fix_install is changed + + - name: Install package with version, without executable path + pnpm: + name: "{{ package }}" + version: 0.1.3 + path: "{{ remote_tmp_dir }}" + state: present + environment: + PATH: "{{ pnpm_bin_path }}:{{ node_bin_path }}:{{ ansible_env.PATH }}" + register: pnpm_install + + - name: Assert that package with version is installed + ansible.builtin.assert: + that: + - pnpm_install is success + - pnpm_install is changed + + - name: Reinstall package with version, without explicit executable path + pnpm: + name: "{{ package }}" + version: 0.1.3 + path: "{{ remote_tmp_dir }}" + state: present + environment: + PATH: "{{ pnpm_bin_path }}:{{ node_bin_path }}:{{ ansible_env.PATH }}" + register: pnpm_reinstall + + - name: Assert that there is no change + ansible.builtin.assert: + that: + - pnpm_reinstall is success + - not (pnpm_reinstall is changed) + + - name: Update package, without executable path + pnpm: + name: "{{ package }}" + path: "{{ remote_tmp_dir }}" + state: latest + environment: + PATH: "{{ pnpm_bin_path }}:{{ node_bin_path }}:{{ ansible_env.PATH }}" + register: pnpm_update + + - name: Assert that result is changed and successful + ansible.builtin.assert: + that: + - pnpm_update is success + - pnpm_update is changed + + - name: Remove package, without executable path + pnpm: + name: "{{ package }}" + path: "{{ remote_tmp_dir }}" + state: absent + environment: + PATH: "{{ pnpm_bin_path }}:{{ node_bin_path }}:{{ ansible_env.PATH }}" + register: pnpm_absent + + - name: Assert that result is changed and successful + ansible.builtin.assert: + that: + - pnpm_absent is success + - pnpm_absent is changed + + - name: Install package with version and alias, without executable path + pnpm: + name: "{{ package }}" + alias: tailwind-1 + version: 0.1.3 + path: "{{ remote_tmp_dir }}" + state: present + environment: + PATH: "{{ pnpm_bin_path }}:{{ node_bin_path }}:{{ ansible_env.PATH }}" + register: pnpm_install + + - name: Assert that package with version and alias is installed + ansible.builtin.assert: + that: + - pnpm_install is success + - pnpm_install is changed + + - name: Reinstall package with version and alias, without explicit executable path + pnpm: + name: "{{ package }}" + alias: tailwind-1 + version: 0.1.3 + path: "{{ remote_tmp_dir }}" + state: present + environment: + PATH: "{{ pnpm_bin_path }}:{{ node_bin_path }}:{{ ansible_env.PATH }}" + register: pnpm_reinstall + + - name: Assert that there is no change + ansible.builtin.assert: + that: + - pnpm_reinstall is success + - not (pnpm_reinstall is changed) + + - name: Remove package with alias, without executable path + pnpm: + name: tailwindcss + alias: tailwind-1 + path: "{{ remote_tmp_dir }}" + state: absent + environment: + PATH: "{{ pnpm_bin_path }}:{{ node_bin_path }}:{{ ansible_env.PATH }}" + register: pnpm_absent + + - name: Assert that result is changed and successful + ansible.builtin.assert: + that: + - pnpm_absent is success + - pnpm_absent is changed + + - name: Install package without dependency globally + pnpm: + name: "{{ package }}" + executable: "{{ pnpm_bin_path }}/pnpm" + state: present + global: true + environment: + PATH: "{{ pnpm_bin_path }}:{{ node_bin_path }}:{{ ansible_env.PATH }}" + PNPM_HOME: "{{ pnpm_bin_path }}" + register: pnpm_install + + - name: Assert that result is changed and successful + ansible.builtin.assert: + that: + - pnpm_install is success + - pnpm_install is changed + + - name: Reinstall package globally, without explicit executable path + pnpm: + name: "{{ package }}" + state: present + global: true + environment: + PATH: "{{ pnpm_bin_path }}:{{ node_bin_path }}:{{ ansible_env.PATH }}" + PNPM_HOME: "{{ pnpm_bin_path }}" + register: pnpm_reinstall + + - name: Assert that there is no change + ansible.builtin.assert: + that: + - pnpm_reinstall is success + - not (pnpm_reinstall is changed) + + - name: Remove package without dependency globally + pnpm: + name: "{{ package }}" + executable: "{{ pnpm_bin_path }}/pnpm" + global: true + state: absent + environment: + PATH: "{{ pnpm_bin_path }}:{{ node_bin_path }}:{{ ansible_env.PATH }}" + PNPM_HOME: "{{ pnpm_bin_path }}" + register: pnpm_absent + + - name: Assert that result is changed and successful + ansible.builtin.assert: + that: + - pnpm_absent is success + - pnpm_absent is changed diff --git a/tests/integration/targets/pnpm/templates/package.j2 b/tests/integration/targets/pnpm/templates/package.j2 new file mode 100644 index 0000000000..429ea9bee2 --- /dev/null +++ b/tests/integration/targets/pnpm/templates/package.j2 @@ -0,0 +1,13 @@ +{# +Copyright (c) Ansible Project +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +#} +{ + "name": "ansible-pnpm-testing", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "reading-time": "^1.5.0" + } +}