From 38d49d240eb03ef64a2d815295ed9cb3fee3baae Mon Sep 17 00:00:00 2001 From: Alexei Znamensky <103110+russoz@users.noreply.github.com> Date: Thu, 7 May 2026 05:25:10 +1200 Subject: [PATCH] yarn: add Alpine Linux support in integration tests (#11943) * test(yarn): add Alpine Linux support via apk Install nodejs and yarn via apk on Alpine, sharing the functional test block with the existing non-Alpine (pre-built binary) path. Extracts the test block into tests.yml to avoid duplication. Fixes #4270 Co-Authored-By: Claude Sonnet 4.6 * fix(yarn): skip Node.js runtime warnings in stderr processing Node.js 24 emits DeprecationWarning lines to stderr (e.g. for url.parse()) that are not JSON, causing _process_yarn_error to fail with "Unexpected stderr output from Yarn". Skip lines starting with "(node:" before attempting JSON parsing. Co-Authored-By: Claude Sonnet 4.6 * test(yarn): add changelog fragment for #11943 Co-Authored-By: Claude Sonnet 4.6 * fix(yarn): only JSON-parse lines starting with '{' in stderr Node.js 24 emits multi-line DeprecationWarnings to stderr (e.g. the hint line "(Use `node --trace-deprecation ...`") that are not JSON and were tripping the "Unexpected stderr output from Yarn" failure. Yarn's structured output always starts with '{', so skip any line that doesn't. Co-Authored-By: Claude Sonnet 4.6 * test(yarn): install sqlite on Alpine to fix nodejs 22 symbol error On Alpine 3.21 nodejs 22 requires SQLite session extension symbols (sqlite3session_*) that are not present in sqlite-libs; installing the full sqlite package provides them. Co-Authored-By: Claude Sonnet 4.6 * test(yarn): refresh apk cache and upgrade sqlite-libs before installing nodejs The CI Alpine container may have a stale sqlite-libs that lacks the session extension symbols (sqlite3session_*) required by nodejs 22+. Force a cache refresh and upgrade sqlite-libs to the latest revision. Co-Authored-By: Claude Sonnet 4.6 * fix(yarn): warn on non-JSON stderr lines instead of silently skipping Non-JSON lines in stderr (e.g. Node.js runtime DeprecationWarnings) are surfaced to the user via module.warn() rather than being silently ignored, since their content and meaning are not known in advance. Co-Authored-By: Claude Sonnet 4.6 * prefix yarn output line * Update changelogs/fragments/11943-yarn-nodejs-runtime-warnings.yml Co-authored-by: Felix Fontein --------- Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Felix Fontein --- .../11943-yarn-nodejs-runtime-warnings.yml | 2 + plugins/modules/yarn.py | 7 +- tests/integration/targets/yarn/tasks/main.yml | 5 +- tests/integration/targets/yarn/tasks/run.yml | 199 +---------------- .../targets/yarn/tasks/run_alpine.yml | 25 +++ .../integration/targets/yarn/tasks/tests.yml | 201 ++++++++++++++++++ 6 files changed, 240 insertions(+), 199 deletions(-) create mode 100644 changelogs/fragments/11943-yarn-nodejs-runtime-warnings.yml create mode 100644 tests/integration/targets/yarn/tasks/run_alpine.yml create mode 100644 tests/integration/targets/yarn/tasks/tests.yml diff --git a/changelogs/fragments/11943-yarn-nodejs-runtime-warnings.yml b/changelogs/fragments/11943-yarn-nodejs-runtime-warnings.yml new file mode 100644 index 0000000000..06f633dd1d --- /dev/null +++ b/changelogs/fragments/11943-yarn-nodejs-runtime-warnings.yml @@ -0,0 +1,2 @@ +bugfixes: + - yarn - skip Node.js runtime warning lines (starting with ``(node:``) in stderr before JSON parsing, fixing failures with Node.js 24 which emits ``DeprecationWarning`` to stderr. The warnings are passed on to the user (https://github.com/ansible-collections/community.general/pull/11943). diff --git a/plugins/modules/yarn.py b/plugins/modules/yarn.py index 437104f4b1..406a6c8487 100644 --- a/plugins/modules/yarn.py +++ b/plugins/modules/yarn.py @@ -187,8 +187,13 @@ class Yarn: def _process_yarn_error(self, err): try: - # We need to filter for errors, since Yarn warnings are included in stderr + # We need to filter for errors, since Yarn warnings are included in stderr. + # Non-JSON lines (e.g. Node.js runtime warnings) are surfaced via module.warn() + # rather than treated as errors, since their meaning is unknown. for line in err.splitlines(): + if not line.startswith("{"): + self.module.warn(f"yarn stderr: {line}") + continue if json.loads(line)["type"] == "error": self.module.fail_json(msg=err) except Exception: diff --git a/tests/integration/targets/yarn/tasks/main.yml b/tests/integration/targets/yarn/tasks/main.yml index df6cc50411..8180d375eb 100644 --- a/tests/integration/targets/yarn/tasks/main.yml +++ b/tests/integration/targets/yarn/tasks/main.yml @@ -11,6 +11,9 @@ # ============================================================ +- include_tasks: run_alpine.yml + when: ansible_facts.os_family == 'Alpine' + - include_tasks: run.yml vars: nodejs_version: '{{ item.node_version }}' @@ -22,4 +25,4 @@ with_items: - {node_version: 16.20.2, yarn_version: 1.22.22} # oldest node version with macOS arm64 support when: - - not (ansible_facts.os_family == 'Alpine') # TODO + - not (ansible_facts.os_family == 'Alpine') diff --git a/tests/integration/targets/yarn/tasks/run.yml b/tests/integration/targets/yarn/tasks/run.yml index 21857b6a3e..ef90715654 100644 --- a/tests/integration/targets/yarn/tasks/run.yml +++ b/tests/integration/targets/yarn/tasks/run.yml @@ -35,203 +35,8 @@ path: '{{remote_tmp_dir}}/node_modules' state: absent -# Set vars for our test harness -- vars: - # node_bin_path: "/usr/local/lib/nodejs/node-v{{nodejs_version}}/bin" +- include_tasks: tests.yml + vars: node_bin_path: "/usr/local/lib/nodejs/{{ nodejs_path }}/bin" yarn_bin_path: "{{ remote_tmp_dir }}/yarn-v{{ yarn_version }}/bin" package: 'iconv-lite' - environment: - PATH: "{{ node_bin_path }}:{{ansible_facts.env.PATH}}" - YARN_IGNORE_ENGINES: true - block: - - # Get the version of Yarn and register to a variable - - shell: '{{ yarn_bin_path }}/yarn --version' - environment: - PATH: '{{ node_bin_path }}:{{ ansible_facts.env.PATH }}' - register: yarn_version - - - name: 'Create dummy package.json' - template: - src: package.j2 - dest: '{{ remote_tmp_dir }}/package.json' - - - name: 'Install all packages.' - yarn: - path: '{{ remote_tmp_dir }}' - executable: '{{ yarn_bin_path }}/yarn' - state: present - environment: - PATH: '{{ node_bin_path }}:{{ ansible_facts.env.PATH }}' - - - name: 'Install the same package from package.json again.' - yarn: - path: '{{ remote_tmp_dir }}' - executable: '{{ yarn_bin_path }}/yarn' - name: '{{ package }}' - state: present - environment: - PATH: '{{ node_bin_path }}:{{ ansible_facts.env.PATH }}' - register: yarn_install - - - assert: - that: - - not (yarn_install is changed) - - - name: 'Install all packages in check mode.' - yarn: - path: '{{ remote_tmp_dir }}' - executable: '{{ yarn_bin_path }}/yarn' - state: present - environment: - PATH: '{{ node_bin_path }}:{{ ansible_facts.env.PATH }}' - check_mode: true - register: yarn_install_check - - - name: verify test yarn global installation in check mode - assert: - that: - - yarn_install_check.err is defined - - yarn_install_check.out is defined - - yarn_install_check.err is none - - yarn_install_check.out is none - - - name: 'Install package with explicit version (older version of package)' - yarn: - path: '{{ remote_tmp_dir }}' - executable: '{{ yarn_bin_path }}/yarn' - name: left-pad - version: 1.1.0 - state: present - environment: - PATH: '{{ node_bin_path }}:{{ ansible_facts.env.PATH }}' - register: yarn_install_old_package - - - assert: - 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_facts.env.PATH }}' - - - name: 'Upgrade old package' - yarn: - path: '{{ remote_tmp_dir }}' - executable: '{{ yarn_bin_path }}/yarn' - name: left-pad - state: latest - environment: - PATH: '{{ node_bin_path }}:{{ ansible_facts.env.PATH }}' - register: yarn_update_old_package - - - assert: - that: - - yarn_update_old_package is changed - - - name: 'Remove a package' - yarn: - path: '{{ remote_tmp_dir }}' - executable: '{{ yarn_bin_path }}/yarn' - name: '{{ package }}' - state: absent - environment: - PATH: '{{ node_bin_path }}:{{ ansible_facts.env.PATH }}' - register: yarn_uninstall_package - - - name: 'Assert package removed' - assert: - that: - - yarn_uninstall_package is changed - - - name: 'Global install binary with explicit version (older version of package)' - yarn: - global: true - executable: '{{ yarn_bin_path }}/yarn' - name: prettier - version: 2.0.0 - state: present - environment: - PATH: '{{ node_bin_path }}:{{ ansible_facts.env.PATH }}' - register: yarn_global_install_old_binary - - - assert: - that: - - yarn_global_install_old_binary is changed - - - name: 'Global upgrade old binary' - yarn: - global: true - executable: '{{ yarn_bin_path }}/yarn' - name: prettier - state: latest - environment: - PATH: '{{ node_bin_path }}:{{ ansible_facts.env.PATH }}' - register: yarn_global_update_old_binary - - - assert: - that: - - yarn_global_update_old_binary is changed - - - name: 'Global remove a binary' - yarn: - global: true - executable: '{{ yarn_bin_path }}/yarn' - name: prettier - state: absent - environment: - PATH: '{{ node_bin_path }}:{{ ansible_facts.env.PATH }}' - register: yarn_global_uninstall_binary - - - assert: - that: - - yarn_global_uninstall_binary is changed - - - name: 'Global install package with no binary with explicit version (older version of package)' - yarn: - global: true - executable: '{{ yarn_bin_path }}/yarn' - name: left-pad - version: 1.1.0 - state: present - environment: - PATH: '{{ node_bin_path }}:{{ ansible_facts.env.PATH }}' - register: yarn_global_install_old_package - - - assert: - that: - - yarn_global_install_old_package is changed - - - name: 'Global upgrade old package with no binary' - yarn: - global: true - executable: '{{ yarn_bin_path }}/yarn' - name: left-pad - state: latest - environment: - PATH: '{{ node_bin_path }}:{{ ansible_facts.env.PATH }}' - register: yarn_global_update_old_package - - - assert: - that: - - yarn_global_update_old_package is changed - - - name: 'Global remove a package with no binary' - yarn: - global: true - executable: '{{ yarn_bin_path }}/yarn' - name: left-pad - state: absent - environment: - PATH: '{{ node_bin_path }}:{{ ansible_facts.env.PATH }}' - register: yarn_global_uninstall_package - - - assert: - that: - - yarn_global_uninstall_package is changed diff --git a/tests/integration/targets/yarn/tasks/run_alpine.yml b/tests/integration/targets/yarn/tasks/run_alpine.yml new file mode 100644 index 0000000000..e76a72c951 --- /dev/null +++ b/tests/integration/targets/yarn/tasks/run_alpine.yml @@ -0,0 +1,25 @@ +--- +# 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: Install nodejs and yarn via apk + community.general.apk: + name: + - sqlite-libs + - nodejs + - yarn + state: latest + update_cache: true + +# Clean up before running tests +- name: Remove any previous Nodejs modules + file: + path: '{{ remote_tmp_dir }}/node_modules' + state: absent + +- include_tasks: tests.yml + vars: + node_bin_path: /usr/bin + yarn_bin_path: /usr/bin + package: 'iconv-lite' diff --git a/tests/integration/targets/yarn/tasks/tests.yml b/tests/integration/targets/yarn/tasks/tests.yml new file mode 100644 index 0000000000..8710c2afc4 --- /dev/null +++ b/tests/integration/targets/yarn/tasks/tests.yml @@ -0,0 +1,201 @@ +--- +# 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 + +# Expects: node_bin_path, yarn_bin_path, package + +- environment: + PATH: "{{ node_bin_path }}:{{ ansible_facts.env.PATH }}" + YARN_IGNORE_ENGINES: true + block: + + # Get the version of Yarn and register to a variable + - shell: '{{ yarn_bin_path }}/yarn --version' + environment: + PATH: '{{ node_bin_path }}:{{ ansible_facts.env.PATH }}' + register: yarn_installed_version + + - name: 'Create dummy package.json' + template: + src: package.j2 + dest: '{{ remote_tmp_dir }}/package.json' + + - name: 'Install all packages.' + yarn: + path: '{{ remote_tmp_dir }}' + executable: '{{ yarn_bin_path }}/yarn' + state: present + environment: + PATH: '{{ node_bin_path }}:{{ ansible_facts.env.PATH }}' + + - name: 'Install the same package from package.json again.' + yarn: + path: '{{ remote_tmp_dir }}' + executable: '{{ yarn_bin_path }}/yarn' + name: '{{ package }}' + state: present + environment: + PATH: '{{ node_bin_path }}:{{ ansible_facts.env.PATH }}' + register: yarn_install + + - assert: + that: + - not (yarn_install is changed) + + - name: 'Install all packages in check mode.' + yarn: + path: '{{ remote_tmp_dir }}' + executable: '{{ yarn_bin_path }}/yarn' + state: present + environment: + PATH: '{{ node_bin_path }}:{{ ansible_facts.env.PATH }}' + check_mode: true + register: yarn_install_check + + - name: verify test yarn global installation in check mode + assert: + that: + - yarn_install_check.err is defined + - yarn_install_check.out is defined + - yarn_install_check.err is none + - yarn_install_check.out is none + + - name: 'Install package with explicit version (older version of package)' + yarn: + path: '{{ remote_tmp_dir }}' + executable: '{{ yarn_bin_path }}/yarn' + name: left-pad + version: 1.1.0 + state: present + environment: + PATH: '{{ node_bin_path }}:{{ ansible_facts.env.PATH }}' + register: yarn_install_old_package + + - assert: + 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_facts.env.PATH }}' + + - name: 'Upgrade old package' + yarn: + path: '{{ remote_tmp_dir }}' + executable: '{{ yarn_bin_path }}/yarn' + name: left-pad + state: latest + environment: + PATH: '{{ node_bin_path }}:{{ ansible_facts.env.PATH }}' + register: yarn_update_old_package + + - assert: + that: + - yarn_update_old_package is changed + + - name: 'Remove a package' + yarn: + path: '{{ remote_tmp_dir }}' + executable: '{{ yarn_bin_path }}/yarn' + name: '{{ package }}' + state: absent + environment: + PATH: '{{ node_bin_path }}:{{ ansible_facts.env.PATH }}' + register: yarn_uninstall_package + + - name: 'Assert package removed' + assert: + that: + - yarn_uninstall_package is changed + + - name: 'Global install binary with explicit version (older version of package)' + yarn: + global: true + executable: '{{ yarn_bin_path }}/yarn' + name: prettier + version: 2.0.0 + state: present + environment: + PATH: '{{ node_bin_path }}:{{ ansible_facts.env.PATH }}' + register: yarn_global_install_old_binary + + - assert: + that: + - yarn_global_install_old_binary is changed + + - name: 'Global upgrade old binary' + yarn: + global: true + executable: '{{ yarn_bin_path }}/yarn' + name: prettier + state: latest + environment: + PATH: '{{ node_bin_path }}:{{ ansible_facts.env.PATH }}' + register: yarn_global_update_old_binary + + - assert: + that: + - yarn_global_update_old_binary is changed + + - name: 'Global remove a binary' + yarn: + global: true + executable: '{{ yarn_bin_path }}/yarn' + name: prettier + state: absent + environment: + PATH: '{{ node_bin_path }}:{{ ansible_facts.env.PATH }}' + register: yarn_global_uninstall_binary + + - assert: + that: + - yarn_global_uninstall_binary is changed + + - name: 'Global install package with no binary with explicit version (older version of package)' + yarn: + global: true + executable: '{{ yarn_bin_path }}/yarn' + name: left-pad + version: 1.1.0 + state: present + environment: + PATH: '{{ node_bin_path }}:{{ ansible_facts.env.PATH }}' + register: yarn_global_install_old_package + + - assert: + that: + - yarn_global_install_old_package is changed + + - name: 'Global upgrade old package with no binary' + yarn: + global: true + executable: '{{ yarn_bin_path }}/yarn' + name: left-pad + state: latest + environment: + PATH: '{{ node_bin_path }}:{{ ansible_facts.env.PATH }}' + register: yarn_global_update_old_package + + - assert: + that: + - yarn_global_update_old_package is changed + + - name: 'Global remove a package with no binary' + yarn: + global: true + executable: '{{ yarn_bin_path }}/yarn' + name: left-pad + state: absent + environment: + PATH: '{{ node_bin_path }}:{{ ansible_facts.env.PATH }}' + register: yarn_global_uninstall_package + + - assert: + that: + - yarn_global_uninstall_package is changed