Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package ansible-core-2.20 for openSUSE:Factory checked in at 2026-11-06 19:55:29 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/ansible-core-2.20 (Old) and /work/SRC/openSUSE:Factory/.ansible-core-2.20.new.3759290 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "ansible-core-2.20" Fri Nov 6 19:55:29 2026 rev:8 rq:1395743 version:2.20.11 Changes: -------- --- /work/SRC/openSUSE:Factory/ansible-core-2.20/ansible-core-2.20.changes 2026-10-18 19:25:21.583874649 +0200 +++ /work/SRC/openSUSE:Factory/.ansible-core-2.20.new.3759290/ansible-core-2.20.changes 2026-11-06 20:00:50.702385632 +0100 @@ -1,0 +2,27 @@ +Fri Nov 6 08:45:45 UTC 2026 - Johannes Kastl <[email protected]> + +- update to 2.20.11: + https://github.com/ansible/ansible/blob/v2.20.11/changelogs/CHANGELOG-v2.20.rst + * Minor Changes + - ansible-test - Remove unsupported FreeBSD 13.5 remote. + - ansible-test - Replace FreeBSD 14.3 remote with 14.5. + - ansible-test - Replace RHEL 10.1 remote with 10.2. + - ansible-test - Replace RHEL 9.7 remote with 9.8. + * Security Fixes + - ansible-galaxy collection install - fix validating FILES.json + entries are contained in the collection before creating them. + (CVE-2026-89091) + - ansible-galaxy role install - fix validating the role name is + contained to the configured role path. (CVE-2026-105929) + * Bugfixes + - ansible-test - Restore code coverage reporting for Python + code residing in integration tests. + - git module - updated to avoid possible incorrect parameters + read from the repo targets. CVE-2026-84503 + - module_utils.mask_url - handle exceptions on parseurl to + avoid disclosing url itself. + - sudo become plugin, sudo-rs compatibility fix for non english + locales +- remove patch ansible-core-CVE-2026-89091.patch + +------------------------------------------------------------------- Old: ---- ansible-core-CVE-2026-89091.patch ansible_core-2.20.10.tar.gz ansible_core-2.20.10.tar.gz.sha256 New: ---- ansible_core-2.20.11.tar.gz ansible_core-2.20.11.tar.gz.sha256 ----------(Old B)---------- Old: locales - remove patch ansible-core-CVE-2026-89091.patch ----------(Old E)---------- ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ ansible-core-2.20.spec ++++++ --- /var/tmp/diff_new_pack.mzShoi/_old 2026-11-06 20:00:52.331453826 +0100 +++ /var/tmp/diff_new_pack.mzShoi/_new 2026-11-06 20:00:52.333453910 +0100 @@ -43,7 +43,7 @@ %endif Name: ansible-core-2.20 -Version: 2.20.10 +Version: 2.20.11 Release: 0 Summary: Radically simple IT automation License: GPL-3.0-or-later @@ -52,8 +52,6 @@ Source1: ansible_core-%{version}.tar.gz.sha256 # CVE-2026-16493 (bsc#1272369) Patch0: ansible-core-CVE-2026-16493.patch -# CVE-2026-89091 (bsc#1284847) -Patch1: ansible-core-CVE-2026-89091.patch BuildArch: noarch Provides: ansible-core = %{version} ++++++ ansible_core-2.20.10.tar.gz -> ansible_core-2.20.11.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/PKG-INFO new/ansible_core-2.20.11/PKG-INFO --- old/ansible_core-2.20.10/PKG-INFO 2026-10-05 17:06:40.000000000 +0200 +++ new/ansible_core-2.20.11/PKG-INFO 2026-11-02 18:10:51.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: ansible-core -Version: 2.20.10 +Version: 2.20.11 Summary: Radically simple IT automation Author: Ansible Project Project-URL: Homepage, https://ansible.com/ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/ansible_core.egg-info/PKG-INFO new/ansible_core-2.20.11/ansible_core.egg-info/PKG-INFO --- old/ansible_core-2.20.10/ansible_core.egg-info/PKG-INFO 2026-10-05 17:06:40.000000000 +0200 +++ new/ansible_core-2.20.11/ansible_core.egg-info/PKG-INFO 2026-11-02 18:10:51.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: ansible-core -Version: 2.20.10 +Version: 2.20.11 Summary: Radically simple IT automation Author: Ansible Project Project-URL: Homepage, https://ansible.com/ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/ansible_core.egg-info/SOURCES.txt new/ansible_core-2.20.11/ansible_core.egg-info/SOURCES.txt --- old/ansible_core-2.20.10/ansible_core.egg-info/SOURCES.txt 2026-10-05 17:06:40.000000000 +0200 +++ new/ansible_core-2.20.11/ansible_core.egg-info/SOURCES.txt 2026-11-02 18:10:51.000000000 +0100 @@ -998,11 +998,13 @@ test/integration/targets/ansible-galaxy-collection/library/setup_collections.py test/integration/targets/ansible-galaxy-collection/meta/main.yml test/integration/targets/ansible-galaxy-collection/tasks/build.yml +test/integration/targets/ansible-galaxy-collection/tasks/build_and_install_tarball.yml test/integration/targets/ansible-galaxy-collection/tasks/download.yml test/integration/targets/ansible-galaxy-collection/tasks/fail_fast_resolvelib.yml test/integration/targets/ansible-galaxy-collection/tasks/init.yml test/integration/targets/ansible-galaxy-collection/tasks/install.yml test/integration/targets/ansible-galaxy-collection/tasks/install_offline.yml +test/integration/targets/ansible-galaxy-collection/tasks/install_tarball.yml test/integration/targets/ansible-galaxy-collection/tasks/list.yml test/integration/targets/ansible-galaxy-collection/tasks/main.yml test/integration/targets/ansible-galaxy-collection/tasks/pinned_pre_releases_in_deptree.yml @@ -1124,6 +1126,10 @@ test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/tests/integration/targets/win_collection/module_utils/Ansible.ModuleUtils.AdjacentPwshCoverage.psm1 test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/tests/integration/targets/win_collection/tasks/main.yml test/integration/targets/ansible-test-coverage/ansible_collections/ns/col/plugins/module_utils/test_util.py +test/integration/targets/ansible-test-coverage/ansible_collections/ns/col/tests/integration/targets/hello/aliases +test/integration/targets/ansible-test-coverage/ansible_collections/ns/col/tests/integration/targets/hello/runme.sh +test/integration/targets/ansible-test-coverage/ansible_collections/ns/col/tests/integration/targets/hello/world.py +test/integration/targets/ansible-test-coverage/ansible_collections/ns/col/tests/unit/test_something.py test/integration/targets/ansible-test-debugging/aliases test/integration/targets/ansible-test-debugging-env/aliases test/integration/targets/ansible-test-debugging-env/runme.sh @@ -4957,7 +4963,6 @@ test/units/cli/test_playbook.py test/units/cli/test_vault.py test/units/cli/arguments/test_option_helpers.py -test/units/cli/galaxy/test_collection_extract_tar.py test/units/cli/galaxy/test_display_collection.py test/units/cli/galaxy/test_display_header.py test/units/cli/galaxy/test_display_role.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/changelogs/CHANGELOG-v2.20.rst new/ansible_core-2.20.11/changelogs/CHANGELOG-v2.20.rst --- old/ansible_core-2.20.10/changelogs/CHANGELOG-v2.20.rst 2026-10-05 17:06:40.000000000 +0200 +++ new/ansible_core-2.20.11/changelogs/CHANGELOG-v2.20.rst 2026-11-02 18:10:51.000000000 +0100 @@ -4,6 +4,37 @@ .. contents:: Topics +v2.20.11 +======== + +Release Summary +--------------- + +| Release Date: 2026-11-02 +| `Porting Guide <https://docs.ansible.com/ansible-core/2.20/porting_guides/porting_guide_core_2.20.html>`__ + +Minor Changes +------------- + +- ansible-test - Remove unsupported FreeBSD 13.5 remote. +- ansible-test - Replace FreeBSD 14.3 remote with 14.5. +- ansible-test - Replace RHEL 10.1 remote with 10.2. +- ansible-test - Replace RHEL 9.7 remote with 9.8. + +Security Fixes +-------------- + +- ``ansible-galaxy collection install`` - fix validating ``FILES.json`` entries are contained in the collection before creating them. (CVE-2026-89091) +- ``ansible-galaxy role install`` - fix validating the role name is contained to the configured role path. (CVE-2026-105929) + +Bugfixes +-------- + +- ansible-test - Restore code coverage reporting for Python code residing in integration tests. +- git module - updated to avoid possible incorrect parameters read from the repo targets. CVE-2026-84503 +- module_utils.mask_url - handle exceptions on parseurl to avoid disclosing url itself. +- sudo become plugin, sudo-rs compatibility fix for non english locales + v2.20.10 ======== diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/changelogs/changelog.yaml new/ansible_core-2.20.11/changelogs/changelog.yaml --- old/ansible_core-2.20.10/changelogs/changelog.yaml 2026-10-05 17:06:40.000000000 +0200 +++ new/ansible_core-2.20.11/changelogs/changelog.yaml 2026-11-02 18:10:51.000000000 +0100 @@ -440,6 +440,55 @@ - dnf5-download_dir-download_only.yml - ensure_conn_cli_dir_perms.yml release_date: '2026-09-29' + 2.20.11: + changes: + release_summary: '| Release Date: 2026-11-02 + + | `Porting Guide <https://docs.ansible.com/ansible-core/2.20/porting_guides/porting_guide_core_2.20.html>`__ + + ' + codename: Good Times Bad Times + fragments: + - 2.20.11_summary.yaml + release_date: '2026-11-02' + 2.20.11rc1: + changes: + bugfixes: + - ansible-test - Restore code coverage reporting for Python code residing in + integration tests. + - git module - updated to avoid possible incorrect parameters read from the + repo targets. CVE-2026-84503 + - module_utils.mask_url - handle exceptions on parseurl to avoid disclosing + url itself. + - sudo become plugin, sudo-rs compatibility fix for non english locales + minor_changes: + - ansible-test - Remove unsupported FreeBSD 13.5 remote. + - ansible-test - Replace FreeBSD 14.3 remote with 14.5. + - ansible-test - Replace RHEL 10.1 remote with 10.2. + - ansible-test - Replace RHEL 9.7 remote with 9.8. + release_summary: '| Release Date: 2026-10-27 + + | `Porting Guide <https://docs.ansible.com/ansible-core/2.20/porting_guides/porting_guide_core_2.20.html>`__ + + ' + security_fixes: + - '``ansible-galaxy collection install`` - fix validating ``FILES.json`` entries + are contained in the collection before creating them. (CVE-2026-89091)' + - '``ansible-galaxy role install`` - fix validating the role name is contained + to the configured role path. (CVE-2026-105929)' + codename: Good Times Bad Times + fragments: + - 2.20.11rc1_summary.yaml + - ansible-galaxy-collection-check-path-containment.yml + - ansible-galaxy-role-name-containment.yml + - ansible-test-freebsd.yml + - ansible-test-integration-coverage.yml + - ansible-test-remove-freebsd-13.5.yml + - ansible-test-rhel.yml + - git_fixes.yml + - mask_url_except.yml + - sudo_moar_rs.yml + release_date: '2026-10-27' 2.20.1rc1: changes: bugfixes: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/lib/ansible/cli/galaxy.py new/ansible_core-2.20.11/lib/ansible/cli/galaxy.py --- old/ansible_core-2.20.10/lib/ansible/cli/galaxy.py 2026-10-05 17:06:40.000000000 +0200 +++ new/ansible_core-2.20.11/lib/ansible/cli/galaxy.py 2026-11-02 18:10:51.000000000 +0100 @@ -60,6 +60,7 @@ from ansible.template import trust_as_template from ansible.utils.collection_loader import AnsibleCollectionConfig from ansible.utils.display import Display +from ansible.utils.path import is_subpath from ansible.utils.plugin_docs import get_versioned_doclink from ansible.utils.vars import load_extra_vars @@ -1462,6 +1463,15 @@ display.vvv('Processing role %s ' % role.name) + if not any( + is_subpath(os.path.join(path, role.name), path, real=True) + for path in context.CLIARGS['roles_path'] + ): + display.warning(f"The role name '{role.name}' resolves to a path outside of the configured role directory") + self.exit_without_ignore() + display.display(f'- skipping role {role.name}') + continue + # query the galaxy API for the role data if role.install_info is not None: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/lib/ansible/galaxy/collection/__init__.py new/ansible_core-2.20.11/lib/ansible/galaxy/collection/__init__.py --- old/ansible_core-2.20.10/lib/ansible/galaxy/collection/__init__.py 2026-10-05 17:06:40.000000000 +0200 +++ new/ansible_core-2.20.11/lib/ansible/galaxy/collection/__init__.py 2026-11-02 18:10:51.000000000 +0100 @@ -1672,6 +1672,9 @@ except KeyError: raise AnsibleError("Unable to extract '%s' from collection" % dirname) + if not _is_child_path(to_bytes(tar_member.name, errors='surrogate_or_strict'), b_dest): + raise AnsibleError(f"Cannot extract directory '{dirname}' in collection: path is outside of collection '{tar_member.name}'") + b_dir_path = os.path.join(b_dest, to_bytes(dirname, errors='surrogate_or_strict')) b_parent_path = os.path.dirname(b_dir_path) @@ -1776,13 +1779,16 @@ def _is_child_path(path, parent_path, link_name=None): """ Checks that path is a path within the parent_path specified. """ b_path = to_bytes(path, errors='surrogate_or_strict') + b_parent_path = to_bytes(parent_path, errors='surrogate_or_strict') if link_name and not os.path.isabs(b_path): # If link_name is specified, path is the source of the link and we need to resolve the absolute path. b_link_dir = os.path.dirname(to_bytes(link_name, errors='surrogate_or_strict')) - b_path = os.path.abspath(os.path.join(b_link_dir, b_path)) + b_path = os.path.join(b_link_dir, b_path) + elif not os.path.isabs(b_path): + b_path = os.path.join(b_parent_path, b_path) - b_parent_path = to_bytes(parent_path, errors='surrogate_or_strict') + b_path = os.path.normpath(b_path) return b_path == b_parent_path or b_path.startswith(b_parent_path + to_bytes(os.path.sep)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/lib/ansible/module_utils/ansible_release.py new/ansible_core-2.20.11/lib/ansible/module_utils/ansible_release.py --- old/ansible_core-2.20.10/lib/ansible/module_utils/ansible_release.py 2026-10-05 17:06:40.000000000 +0200 +++ new/ansible_core-2.20.11/lib/ansible/module_utils/ansible_release.py 2026-11-02 18:10:51.000000000 +0100 @@ -17,6 +17,6 @@ from __future__ import annotations -__version__ = '2.20.10' +__version__ = '2.20.11' __author__ = 'Ansible, Inc.' __codename__ = "Good Times Bad Times" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/lib/ansible/module_utils/urls.py new/ansible_core-2.20.11/lib/ansible/module_utils/urls.py --- old/ansible_core-2.20.10/lib/ansible/module_utils/urls.py 2026-10-05 17:06:40.000000000 +0200 +++ new/ansible_core-2.20.11/lib/ansible/module_utils/urls.py 2026-11-02 18:10:51.000000000 +0100 @@ -47,6 +47,7 @@ import tempfile import traceback import types # pylint: disable=unused-import +import unicodedata import urllib.error import urllib.request @@ -322,11 +323,18 @@ Safely display a url by masking confidential data from a string or the result from urlparse/split """ - if (parsed_url := urlparse(url)) and not parsed_url.username and not parsed_url.password: - return url + mask = '****' + try: + if (parsed_url := urlparse(url)) and not parsed_url.username and not parsed_url.password: + return url + except Exception as e: + msg = 'mask_url could not parse the url provided' + # skip replace on unicode issues, specially since they can be subsets of url. + if unicodedata.is_normalized('NFKC', url): + msg = f'{msg}: {str(e).replace(url, mask)}' + raise ValueError(msg) netloc: str - mask = '****' if parsed_url.password: netloc = parsed_url.netloc.replace(f'{parsed_url.username}:{parsed_url.password}@', f'{mask}:{mask}@') else: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/lib/ansible/modules/git.py new/ansible_core-2.20.11/lib/ansible/modules/git.py --- old/ansible_core-2.20.10/lib/ansible/modules/git.py 2026-10-05 17:06:40.000000000 +0200 +++ new/ansible_core-2.20.11/lib/ansible/modules/git.py 2026-11-02 18:10:51.000000000 +0100 @@ -380,14 +380,14 @@ module.fail_json(msg="Unable to read %s" % headfile) if rawdata: try: - rawdata = rawdata.replace('refs/remotes/%s' % remote, '', 1) + rawdata = rawdata.replace(f'refs/remotes/{remote}', '', 1) refparts = rawdata.split(' ') newref = refparts[-1] nrefparts = newref.split('/', 2) res = nrefparts[-1].rstrip('\n') except Exception: if fail_on_error and module: - module.fail_json(msg="Unable to split head from '%s'" % rawdata) + module.fail_json(msg=f"Unable to split head from '{rawdata}'") return res @@ -407,7 +407,7 @@ params = [] # run a bad submodule command to get valid params - cmd = "%s submodule update --help" % (git_path) + cmd = [git_path, 'submodule', 'update', '--help'] rc, stdout, stderr = module.run_command(cmd, cwd=cwd) lines = stderr.split('\n') update_line = None @@ -520,7 +520,7 @@ def get_version(module, git_path, dest, ref="HEAD"): """ samples the version of the git repo """ - cmd = "%s rev-parse %s" % (git_path, ref) + cmd = [git_path, "rev-parse", ref] rc, stdout, stderr = module.run_command(cmd, cwd=dest) sha = to_native(stdout).rstrip('\n') return sha @@ -640,7 +640,7 @@ # git before 1.7.5 doesn't have separate-git-dir argument, do fallback needs_separate_git_dir_fallback = True else: - cmd.append('--separate-git-dir=%s' % separate_git_dir) + cmd.append(f'--separate-git-dir={separate_git_dir}') cmd.extend([repo, dest]) module.run_command(cmd, check_rc=True, cwd=dest_dirname) @@ -665,7 +665,7 @@ if bare: return False - cmd = "%s status --porcelain" % (git_path) + cmd = [git_path, "status", "--porcelain"] rc, stdout, stderr = module.run_command(cmd, cwd=dest) lines = stdout.splitlines() lines = list(filter(lambda c: not re.search('^\\?\\?.*$', c), lines)) @@ -679,7 +679,7 @@ Discards any changes to tracked files in working tree since that commit. """ - cmd = "%s reset --hard HEAD" % (git_path,) + cmd = [git_path, "reset", "--hard", "HEAD"] return module.run_command(cmd, check_rc=True, cwd=dest) @@ -691,7 +691,7 @@ # Ensure we have the object we are referring to during git diff ! git_version_used = git_version(git_path, module) fetch(git_path, module, repo, dest, after, remote, depth, bare, '', git_version_used) - cmd = '%s diff %s %s' % (git_path, before, after) + cmd = [git_path, 'diff', before, after] (rc, out, err) = module.run_command(cmd, cwd=dest) if rc == 0 and out: return {'prepared': out} @@ -708,6 +708,7 @@ cloning = False cwd = None tag = False + cmd = [git_path, 'ls-remote', remote] if remote == module.params['repo']: cloning = True elif remote == 'file://' + os.path.expanduser(module.params['repo']): @@ -717,15 +718,15 @@ if version == 'HEAD': if cloning: # cloning the repo, just get the remote's HEAD version - cmd = '%s ls-remote %s -h HEAD' % (git_path, remote) + cmd.extend(['-h', 'HEAD']) else: head_branch = get_head_branch(git_path, module, dest, remote, bare) - cmd = '%s ls-remote %s -h refs/heads/%s' % (git_path, remote, head_branch) + cmd.extend(['-h', '--', shlex.quote(f'refs/heads/{head_branch}')]) elif is_remote_branch(git_path, module, dest, remote, version): - cmd = '%s ls-remote %s -h refs/heads/%s' % (git_path, remote, version) + cmd.extend(['-h', f'refs/heads/{version}']) elif is_remote_tag(git_path, module, dest, remote, version): tag = True - cmd = '%s ls-remote %s -t refs/tags/%s*' % (git_path, remote, version) + cmd.extend(['-t', f'refs/tags/{version}*']) else: # appears to be a sha1. return as-is since it appears # cannot check for a specific sha1 on remote @@ -750,7 +751,7 @@ def is_remote_tag(git_path, module, dest, remote, version): - cmd = '%s ls-remote %s -t refs/tags/%s' % (git_path, remote, version) + cmd = [git_path, 'ls-remote', remote, '-t', f'refs/tags/{version}'] (rc, out, err) = module.run_command(cmd, check_rc=True, cwd=dest) if to_native(version, errors='surrogate_or_strict') in out: return True @@ -760,10 +761,10 @@ def get_branches(git_path, module, dest): branches = [] - cmd = '%s branch --no-color -a' % (git_path,) + cmd = [git_path, 'branch', '--no-color', '-a'] (rc, out, err) = module.run_command(cmd, cwd=dest) if rc != 0: - module.fail_json(msg="Could not determine branch data - received %s" % out, stdout=out, stderr=err) + module.fail_json(msg=f"Could not determine branch data - received {out}", stdout=out, stderr=err) for line in out.split('\n'): if line.strip(): branches.append(line.strip()) @@ -775,7 +776,7 @@ cmd = [git_path, 'for-each-ref', 'refs/tags/', '--format', '%(objecttype):%(refname:short)'] (rc, out, err) = module.run_command(cmd, cwd=dest) if rc != 0: - module.fail_json(msg="Could not determine tag data - received %s" % out, stdout=out, stderr=err) + module.fail_json(msg=f"Could not determine tag data - received {out}", stdout=out, stderr=err) for line in to_native(out).split('\n'): if line.strip(): tagtype, tagname = line.strip().split(':') @@ -785,7 +786,7 @@ def is_remote_branch(git_path, module, dest, remote, version): - cmd = '%s ls-remote %s -h refs/heads/%s' % (git_path, remote, version) + cmd = [git_path, 'ls-remote', remote, '-h', f'refs/heads/{version}'] (rc, out, err) = module.run_command(cmd, check_rc=True, cwd=dest) if to_native(version, errors='surrogate_or_strict') in out: return True @@ -795,13 +796,7 @@ def is_local_branch(git_path, module, dest, branch): branches = get_branches(git_path, module, dest) - lbranch = '%s' % branch - if lbranch in branches: - return True - elif '* %s' % branch in branches: - return True - else: - return False + return f'{branch}' in branches or f'* {branch}' in branches def is_not_a_branch(git_path, module, dest): @@ -863,6 +858,11 @@ if is_not_a_branch(git_path, module, dest): headfile = os.path.join(repo_path, 'refs', 'remotes', remote, 'HEAD') branch = head_splitter(headfile, remote, module=module, fail_on_error=True) + + if branch.startswith('-'): + # prevent branch names that can be confused as options + module.fail_json(msg=f'Invalid branch name ({branch}) from {headfile} in {remote}') + return branch @@ -987,13 +987,13 @@ after = {} for submodule in begin: branch = get_submodule_branch(git_path, module, dest, submodule) - version_ref = f'{remote}/{branch}' if branch != 'HEAD' else 'HEAD' + version_ref = 'HEAD' if branch == 'HEAD' else shlex.quote(f'{remote}/{branch}') submodule_path = os.path.join(dest, submodule) cmd = [git_path, 'rev-parse', version_ref] (rc, out, err) = module.run_command(cmd, cwd=submodule_path) if rc != 0: module.fail_json( - msg='Unable to determine hash of submodule %s at %s' % (submodule, version_ref), + msg=f'Unable to determine hash of submodule {submodule} at {version_ref}', stdout=out, stderr=err, rc=rc) after[submodule] = out.strip() if begin != after: @@ -1041,48 +1041,52 @@ Can't use git remote set-branches, as it is not available in git 1.7.1 (centos6) """ - branchref = "+refs/heads/%s:refs/heads/%s" % (version, version) - branchref += ' +refs/heads/%s:refs/remotes/%s/%s' % (version, remote, version) - cmd = "%s fetch --depth=%s %s %s" % (git_path, depth, remote, branchref) + cmd = [ + git_path, + "fetch", + f"--depth={depth}", + remote, + f"+refs/heads/{version}:refs/heads/{version}", + f"+refs/heads/{version}:refs/remotes/{remote}/{version}", + ] (rc, out, err) = module.run_command(cmd, cwd=dest) if rc != 0: - module.fail_json(msg="Failed to fetch branch from remote: %s" % version, stdout=out, stderr=err, rc=rc) + module.fail_json(msg=f"Failed to fetch branch from remote: {version}", stdout=out, stderr=err, rc=rc) def switch_version(git_path, module, dest, remote, version, verify_commit, depth, gpg_allowlist): cmd = '' if version == 'HEAD': branch = get_head_branch(git_path, module, dest, remote) - (rc, out, err) = module.run_command("%s checkout --force %s" % (git_path, branch), cwd=dest) + (rc, out, err) = module.run_command([git_path, "checkout", "--force", branch], cwd=dest) if rc != 0: - module.fail_json(msg="Failed to checkout branch %s" % branch, - stdout=out, stderr=err, rc=rc) - cmd = "%s reset --hard %s/%s --" % (git_path, remote, branch) + module.fail_json(msg=f"Failed to checkout branch {branch} HEAD", stdout=out, stderr=err, rc=rc) + cmd = [git_path, "reset", "--hard", shlex.quote("/".join([remote, branch]))] else: # FIXME check for local_branch first, should have been fetched already if is_remote_branch(git_path, module, dest, remote, version): + rv = "/".join([remote, version]) if depth and not is_local_branch(git_path, module, dest, version): # git clone --depth implies --single-branch, which makes # the checkout fail if the version changes # fetch the remote branch, to be able to check it out next set_remote_branch(git_path, module, dest, remote, version, depth) if not is_local_branch(git_path, module, dest, version): - cmd = "%s checkout --track -b %s %s/%s" % (git_path, version, remote, version) + cmd = [git_path, "checkout", "--track", "-b", version, rv] else: - (rc, out, err) = module.run_command("%s checkout --force %s" % (git_path, version), cwd=dest) + (rc, out, err) = module.run_command([git_path, "checkout", "--force", version], cwd=dest) if rc != 0: module.fail_json(msg="Failed to checkout branch %s" % version, stdout=out, stderr=err, rc=rc) - cmd = "%s reset --hard %s/%s" % (git_path, remote, version) + cmd = [git_path, "reset", "--hard", rv] else: - cmd = "%s checkout --force %s" % (git_path, version) + cmd = [git_path, "checkout", "--force", version] (rc, out1, err1) = module.run_command(cmd, cwd=dest) if rc != 0: - if version != 'HEAD': - module.fail_json(msg="Failed to checkout %s" % (version), - stdout=out1, stderr=err1, rc=rc, cmd=cmd) + if version == 'HEAD': + msg = f"Failed to checkout branch {branch}" else: - module.fail_json(msg="Failed to checkout branch %s" % (branch), - stdout=out1, stderr=err1, rc=rc, cmd=cmd) + msg = f"Failed to checkout {version}" + module.fail_json(msg=msg, stdout=out1, stderr=err1, rc=rc, cmd=cmd) if verify_commit: verify_commit_sign(git_path, module, dest, version, gpg_allowlist) @@ -1095,9 +1099,9 @@ git_sub = "verify-tag" else: git_sub = "verify-commit" - cmd = "%s %s %s" % (git_path, git_sub, version) + cmd = [git_path, git_sub, version] if gpg_allowlist: - cmd += " --raw" + cmd.append("--raw") (rc, out, err) = module.run_command(cmd, cwd=dest) if rc != 0: module.fail_json(msg='Failed to verify GPG signature of commit/tag "%s"' % version, stdout=out, stderr=err, rc=rc) @@ -1126,7 +1130,7 @@ def git_version(git_path, module): """return the installed version of git""" - cmd = "%s --version" % git_path + cmd = [git_path, "--version"] (rc, out, err) = module.run_command(cmd) if rc != 0: # one could fail_json here, but the version info is not that important, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/lib/ansible/plugins/become/sudo.py new/ansible_core-2.20.11/lib/ansible/plugins/become/sudo.py --- old/ansible_core-2.20.10/lib/ansible/plugins/become/sudo.py 2026-10-05 17:06:40.000000000 +0200 +++ new/ansible_core-2.20.11/lib/ansible/plugins/become/sudo.py 2026-11-02 18:10:51.000000000 +0100 @@ -101,6 +101,17 @@ fail = ('Sorry, try again.',) missing = ('Sorry, a password is required to run sudo', 'sudo: a password is required') + def check_password_prompt(self, b_output: bytes) -> bool: + # try GNU sudo first + matched = super().check_password_prompt(b_output) + if not matched: + # try sudo-rs, which is not backwards compatible and does i18n + # Sample: '[sudo: [sudo via ansible, key=csfhjsshkagnlthoutcsjownopciscow] password:] Password:' + sudo_rs = re.compile(fr"\[sudo:\s+{re.escape(self.prompt)}\]\s+\S.+\Z".encode()) + # using match as the line MUST start with this + matched = any(sudo_rs.match(l) for l in b_output.splitlines()) + return matched + def build_become_command(self, cmd, shell): super(BecomeModule, self).build_become_command(cmd, shell) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/lib/ansible/release.py new/ansible_core-2.20.11/lib/ansible/release.py --- old/ansible_core-2.20.10/lib/ansible/release.py 2026-10-05 17:06:40.000000000 +0200 +++ new/ansible_core-2.20.11/lib/ansible/release.py 2026-11-02 18:10:51.000000000 +0100 @@ -17,6 +17,6 @@ from __future__ import annotations -__version__ = '2.20.10' +__version__ = '2.20.11' __author__ = 'Ansible, Inc.' __codename__ = "Good Times Bad Times" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/pyproject.toml new/ansible_core-2.20.11/pyproject.toml --- old/ansible_core-2.20.10/pyproject.toml 2026-10-05 17:06:40.000000000 +0200 +++ new/ansible_core-2.20.11/pyproject.toml 2026-11-02 18:10:51.000000000 +0100 @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools >= 66.1.0, <= 84.0.0", "wheel == 0.45.1"] # lower bound to support controller Python versions, upper bound for latest version tested at release +requires = ["setuptools >= 66.1.0, <= 84.2.1", "wheel == 0.45.1"] # lower bound to support controller Python versions, upper bound for latest version tested at release build-backend = "setuptools.build_meta" [project] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/test/integration/targets/ansible-galaxy-collection/files/build_bad_tar.py new/ansible_core-2.20.11/test/integration/targets/ansible-galaxy-collection/files/build_bad_tar.py --- old/ansible_core-2.20.10/test/integration/targets/ansible-galaxy-collection/files/build_bad_tar.py 2026-10-05 17:06:40.000000000 +0200 +++ new/ansible_core-2.20.11/test/integration/targets/ansible-galaxy-collection/files/build_bad_tar.py 2026-11-02 18:10:51.000000000 +0100 @@ -5,11 +5,11 @@ from __future__ import annotations +import argparse import hashlib import io import json import os -import sys import tarfile from ansible.module_utils.common.file import S_IRWXU_RXG_RXO @@ -44,9 +44,23 @@ } -def add_file(tar_file, filename, b_content, update_files=True): +def calculate_target_filename(symlink_path, linkname): + """The target is relative to the symlink.""" + symlink_dir = os.path.dirname(symlink_path) + if symlink_dir: + # tarfile will extract the linkname as {{ symlink_dir }}/{{ linkname }}, even if linkname is an absolute path + return "/".join([symlink_dir, linkname]) + return linkname + + +def add_file(tar_file, filename, b_content, update_files=True, symlink_linkname=None): tar_info = tarfile.TarInfo(filename) - tar_info.size = len(b_content) + if symlink_linkname is not None: + add_file(tar_file, calculate_target_filename(filename, symlink_linkname), b"", update_files=False) + tar_info.type = tarfile.SYMTYPE + tar_info.linkname = symlink_linkname + else: + tar_info.size = len(b_content) tar_info.mode = S_IRWXU_RXG_RXO tar_file.addfile(tarinfo=tar_info, fileobj=io.BytesIO(b_content)) @@ -58,14 +72,76 @@ 'name': filename, 'ftype': 'file', 'chksum_type': 'sha256', - 'chksum_sha256': sha256.hexdigest(), + 'chksum_sha256': sha256.hexdigest() if symlink_linkname is None else None, 'format': 1 }) -collection_tar = os.path.join(sys.argv[1], 'suspicious-test-1.0.0.tar.gz') +def add_dir(tar_file, dir_name, symlink_linkname=None, update_files=True): + if symlink_linkname: + add_dir(tar_file, symlink_linkname, update_files=False) + + tar_info = tarfile.TarInfo(dir_name) + + if symlink_linkname is None: + tar_info.type = tarfile.DIRTYPE + else: + tar_info.type = tarfile.SYMTYPE + tar_info.linkname = symlink_linkname + + tar_info.mode = 0o777 + tar_file.addfile(tar_info) + + if not update_files: + return + + files['files'].append({ + 'name': dir_name, + 'ftype': 'dir', + 'chksum_type': 'sha256', + 'chksum_sha256': None, + 'format': 1, + }) + + +parser = argparse.ArgumentParser(description="Test suspiciously crafted collection artifacts.") +parser.add_argument("dest", help="The destination directory for the collection artifact.") + +mutually_exclusive_group = parser.add_mutually_exclusive_group() +mutually_exclusive_group.add_argument( + "--file", help="A file to add to the collection artifact.", +) +mutually_exclusive_group.add_argument( + "--dir", help="A directory to add to the collection artifact.", +) +mutually_exclusive_group.add_argument( + "--symlink-linkname", help="Add a symlink linkname to the collection artifact.", +) +parser.add_argument( + "--symlink", help="The name of the symlink to use with --symlink-linkname. Defaults to 'symlink'.", +) + + +args = parser.parse_args() + +collection_tar = os.path.join(args.dest, 'suspicious-test-1.0.0.tar.gz') with tarfile.open(collection_tar, mode='w:gz') as tar_file: - add_file(tar_file, '../../outside.sh', b"#!/usr/bin/env bash\necho \"you got pwned\"") + filename = args.file + dirname = args.dir + + if args.symlink and not args.symlink_linkname: + parser.error("--symlink and --symlink-linkname must be used together") + + if args.symlink_linkname: + if args.symlink_linkname.endswith(os.path.sep): + dirname = args.symlink or "symlink" + else: + filename = args.symlink or "symlink" + + if dirname: + add_dir(tar_file, dirname, symlink_linkname=args.symlink_linkname) + elif filename: + add_file(tar_file, filename, b"#!/usr/bin/env bash\necho \"you got pwned\"", symlink_linkname=args.symlink_linkname) b_files = json.dumps(files).encode('utf-8') b_files_hash = hashlib.sha256() @@ -73,12 +149,3 @@ manifest['file_manifest_file']['chksum_sha256'] = b_files_hash.hexdigest() add_file(tar_file, 'FILES.json', b_files) add_file(tar_file, 'MANIFEST.json', json.dumps(manifest).encode('utf-8')) - - b_manifest = json.dumps(manifest).encode('utf-8') - - for name, b in [('MANIFEST.json', b_manifest), ('FILES.json', b_files)]: - b_io = io.BytesIO(b) - tar_info = tarfile.TarInfo(name) - tar_info.size = len(b) - tar_info.mode = 0o0644 - tar_file.addfile(tarinfo=tar_info, fileobj=b_io) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/test/integration/targets/ansible-galaxy-collection/tasks/build_and_install_tarball.yml new/ansible_core-2.20.11/test/integration/targets/ansible-galaxy-collection/tasks/build_and_install_tarball.yml --- old/ansible_core-2.20.10/test/integration/targets/ansible-galaxy-collection/tasks/build_and_install_tarball.yml 1970-01-01 01:00:00.000000000 +0100 +++ new/ansible_core-2.20.11/test/integration/targets/ansible-galaxy-collection/tasks/build_and_install_tarball.yml 2026-11-02 18:10:51.000000000 +0100 @@ -0,0 +1,37 @@ +- name: Test a specially built tarball + debug: + msg: "Creating tarball with arguments: {{ tarfile_arguments }}" + +- name: Create tarball + script: build_bad_tar.py {{ galaxy_dir | quote }} {{ tarfile_arguments }} + +- name: Try to install the tarball + command: ansible-galaxy collection install '{{ galaxy_dir }}/suspicious-test-1.0.0.tar.gz' {{ galaxy_verbosity }} + register: fail_bad_tar + ignore_errors: true + environment: + ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections' + +- name: Get the result of the install attempt + stat: + path: '{{ galaxy_dir }}/ansible_collections/suspicious' + register: fail_bad_tar_actual + +- name: Remove collection if it was installed + file: + path: '{{ galaxy_dir }}/ansible_collections' + state: absent + when: cleanup | default(True) + +- name: Assert the expected result + assert: + fail_msg: "{{ rc_msg }}{{ stderr_msg }}{{ stdout_msg }}" + that: + - fail_bad_tar.rc == expected_rc + - fail_bad_tar.stderr is search(expected_error) + - fail_bad_tar.stdout is search(expected_stdout) + - (expected_rc == 0) | ternary(fail_bad_tar_actual.stat.exists, not fail_bad_tar_actual.stat.exists) + vars: + rc_msg: "Expected rc: {{ expected_rc }}" + stderr_msg: "{{ (expected_error == '') | ternary('', ', error: ' ~ expected_error) }}" + stdout_msg: "{{ (expected_stdout == '') | ternary('', ', stdout: ' ~ expected_stdout) }}" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/test/integration/targets/ansible-galaxy-collection/tasks/install.yml new/ansible_core-2.20.11/test/integration/targets/ansible-galaxy-collection/tasks/install.yml --- old/ansible_core-2.20.10/test/integration/targets/ansible-galaxy-collection/tasks/install.yml 2026-10-05 17:06:40.000000000 +0200 +++ new/ansible_core-2.20.11/test/integration/targets/ansible-galaxy-collection/tasks/install.yml 2026-11-02 18:10:51.000000000 +0100 @@ -285,26 +285,6 @@ - "{{ galaxy_dir }}/scratch/tmp_parent/" - "{{ galaxy_dir }}/tmp_parent-name-1.0.0.tar.gz" -- name: setup bad tarball - {{ test_id }} - script: build_bad_tar.py {{ galaxy_dir | quote }} - -- name: fail to install a collection from a bad tarball - {{ test_id }} - command: ansible-galaxy collection install '{{ galaxy_dir }}/suspicious-test-1.0.0.tar.gz' {{ galaxy_verbosity }} - register: fail_bad_tar - failed_when: fail_bad_tar.rc != 1 and "Cannot extract tar entry '../../outside.sh' as it will be placed outside the collection directory" not in fail_bad_tar.stderr - environment: - ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections' - -- name: get result of failed collection install - {{ test_id }} - stat: - path: '{{ galaxy_dir }}/ansible_collections\suspicious' - register: fail_bad_tar_actual - -- name: assert result of failed collection install - {{ test_id }} - assert: - that: - - not fail_bad_tar_actual.stat.exists - - name: Find artifact url for namespace4.name uri: url: '{{ test_api_server }}v3/plugin/ansible/content/primary/collections/index/namespace4/name/versions/1.0.0/' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/test/integration/targets/ansible-galaxy-collection/tasks/install_tarball.yml new/ansible_core-2.20.11/test/integration/targets/ansible-galaxy-collection/tasks/install_tarball.yml --- old/ansible_core-2.20.10/test/integration/targets/ansible-galaxy-collection/tasks/install_tarball.yml 1970-01-01 01:00:00.000000000 +0100 +++ new/ansible_core-2.20.11/test/integration/targets/ansible-galaxy-collection/tasks/install_tarball.yml 2026-11-02 18:10:51.000000000 +0100 @@ -0,0 +1,97 @@ +- name: Test unsafe tarball extraction + vars: + unsafe_rel_path: '../../outside' + unsafe_abs_path: '{{ galaxy_dir }}/ansible_collections/suspicious/test/../../outside' + safe_linkname: '{{ galaxy_dir }}/ansible_collections/suspicious/test/inside' + expected_rc: 1 + expected_stdout: "" + tarfile_arguments: "{{ item }}" + block: + - name: Test unsafe file extraction + include_tasks: build_and_install_tarball.yml + loop: + - "--file '{{ unsafe_rel_path }}.sh'" + - "--file '{{ unsafe_abs_path }}.sh'" + - "--symlink '{{ unsafe_rel_path }}.sh' --symlink-linkname '{{ safe_linkname }}.sh'" + - "--symlink '{{ unsafe_abs_path }}.sh' --symlink-linkname '{{ safe_linkname }}.sh'" + vars: + expected_error: "Cannot extract tar entry" + + - name: Test unsafe directory extraction + include_tasks: build_and_install_tarball.yml + loop: + - "--dir '{{ unsafe_rel_path }}'" + - "--dir '{{ unsafe_abs_path }}'" + - "--symlink '{{ unsafe_rel_path }}' --symlink-linkname '{{ safe_linkname }}/'" + - "--symlink '{{ unsafe_abs_path }}' --symlink-linkname '{{ safe_linkname }}/'" + vars: + expected_error: "Cannot extract directory" + + - name: Test unsafe symlink linkname + include_tasks: build_and_install_tarball.yml + loop: + - "--symlink-linkname '{{ unsafe_rel_path }}.sh'" + - "--symlink-linkname '{{ unsafe_abs_path }}.sh'" + - "--symlink-linkname '{{ unsafe_rel_path }}/'" + - "--symlink-linkname '{{ unsafe_abs_path }}/'" + - "--symlink 'some_directory/symlink' --symlink-linkname 'safe/../../../unsafe'" + vars: + expected_error: >- + Cannot extract symlink '.*symlink' in collection: path points to location outside of collection + +- name: Test safe tarball extraction + vars: + expected_rc: 0 + expected_error: "" + expected_stdout: "suspicious\\.test:1\\.0\\.0 was installed successfully" + cleanup: False + block: + - name: Test successful directory extraction + include_tasks: build_and_install_tarball.yml + loop: + - "--dir 'safe/'" + - "--dir 'safe'" + loop_control: + loop_var: tarfile_arguments + vars: + cleanup: True + + - name: Test successful relative linkname extraction + include_tasks: build_and_install_tarball.yml + vars: + tarfile_arguments: "--symlink 'some_directory/symlink' --symlink-linkname '../safe'" + + - &stat_symlink + name: Check extracted the symlink exists + stat: + path: "{{ galaxy_dir }}/ansible_collections/suspicious/test/some_directory/symlink" + register: expected_symlink + + - &realpath_symlink + name: Check extracted symlink is not broken + command: "realpath {{ galaxy_dir }}/ansible_collections/suspicious/test/some_directory/symlink" + ignore_errors: True + register: realpath_result + + - &assert_symlink + assert: + that: + - expected_symlink.stat.exists + - realpath_result is not failed + - expected_symlink.stat.lnk_source == realpath_result.stdout == galaxy_dir ~ '/ansible_collections/suspicious/test/safe' + + - &cleanup + name: Remove installed collection + file: + path: "{{ galaxy_dir }}/ansible_collections" + state: absent + + - name: Test successful absolute linkname extraction + include_tasks: build_and_install_tarball.yml + vars: + tarfile_arguments: "--symlink some_directory/symlink --symlink-linkname '{{ galaxy_dir }}/ansible_collections/suspicious/test/safe'" + + - <<: *stat_symlink + - <<: *realpath_symlink + - <<: *assert_symlink + - <<: *cleanup diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/test/integration/targets/ansible-galaxy-collection/tasks/main.yml new/ansible_core-2.20.11/test/integration/targets/ansible-galaxy-collection/tasks/main.yml --- old/ansible_core-2.20.10/test/integration/targets/ansible-galaxy-collection/tasks/main.yml 2026-10-05 17:06:40.000000000 +0200 +++ new/ansible_core-2.20.11/test/integration/targets/ansible-galaxy-collection/tasks/main.yml 2026-11-02 18:10:51.000000000 +0100 @@ -63,6 +63,9 @@ environment: ANSIBLE_CONFIG: '{{ galaxy_dir }}/ansible.cfg' +- name: run ansible-galaxy collection tarball extraction tests + include_tasks: install_tarball.yml + - name: run ansible-galaxy collection publish tests for {{ test_name }} include_tasks: publish.yml args: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/test/integration/targets/ansible-galaxy-role/tasks/main.yml new/ansible_core-2.20.11/test/integration/targets/ansible-galaxy-role/tasks/main.yml --- old/ansible_core-2.20.10/test/integration/targets/ansible-galaxy-role/tasks/main.yml 2026-10-05 17:06:40.000000000 +0200 +++ new/ansible_core-2.20.11/test/integration/targets/ansible-galaxy-role/tasks/main.yml 2026-11-02 18:10:51.000000000 +0100 @@ -63,11 +63,47 @@ register: invalid - assert: - that: "invalid.rc != 0" + that: + - invalid.rc != 0 + - 'invalid.stderr is search("Invalid linkname for tarfile member: path (:?/|/root/invalid) is not a subpath of the role")' - name: Uninstall invalid role command: ansible-galaxy role remove invalid-testrole +- name: Test invalid role requirement name + vars: + abs_name: "{{ remote_tmp_dir }}/unsafe" + install_msg: "{{ abs_name }} was installed successfully" + external_msg: "The role name '{{ abs_name }}' resolves to a path outside of the configured role directory" + block: + - name: Create invalid requirements file + copy: + dest: invalid-requirements.yml + content: |- + - src: "{{ remote_tmp_dir }}/valid-role.tar" + name: "{{ abs_name }}" + + - name: Test installing the role + command: ansible-galaxy install -r invalid-requirements.yml + register: fail_external + ignore_errors: True + + - assert: + that: + - fail_external is failed + - fail_external.stderr is search(external_msg) + - fail_external.stdout is not search(install_msg) + +- name: Test --ignore-errors skips unsatisfiable invalid requirement name + command: ansible-galaxy install {{ remote_tmp_dir }}/roles/../valid-role.tar --roles-path {{ remote_tmp_dir }}/roles --ignore-errors + register: warn_external + +- assert: + that: + - warn_external is success + - warn_external.stderr is search("The role name '.*' resolves to a path outside of the configured role directory") + - warn_external.stdout is not search(".* was installed successfully") + - import_tasks: dir-traversal.yml - import_tasks: valid-role-symlinks.yml - import_tasks: git-config-injection.yml diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/test/integration/targets/ansible-test-container/runme.py new/ansible_core-2.20.11/test/integration/targets/ansible-test-container/runme.py --- old/ansible_core-2.20.10/test/integration/targets/ansible-test-container/runme.py 2026-10-05 17:06:40.000000000 +0200 +++ new/ansible_core-2.20.11/test/integration/targets/ansible-test-container/runme.py 2026-11-02 18:10:51.000000000 +0100 @@ -877,6 +877,7 @@ dict( user='root', password=root_password, + password_lock='false', ), ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/test/integration/targets/ansible-test-coverage/ansible_collections/ns/col/plugins/module_utils/test_util.py new/ansible_core-2.20.11/test/integration/targets/ansible-test-coverage/ansible_collections/ns/col/plugins/module_utils/test_util.py --- old/ansible_core-2.20.10/test/integration/targets/ansible-test-coverage/ansible_collections/ns/col/plugins/module_utils/test_util.py 2026-10-05 17:06:40.000000000 +0200 +++ new/ansible_core-2.20.11/test/integration/targets/ansible-test-coverage/ansible_collections/ns/col/plugins/module_utils/test_util.py 2026-11-02 18:10:51.000000000 +0100 @@ -1,5 +1,4 @@ from __future__ import annotations -def test_coverage(): - pass +A_CONSTANT = True diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/test/integration/targets/ansible-test-coverage/ansible_collections/ns/col/tests/integration/targets/hello/aliases new/ansible_core-2.20.11/test/integration/targets/ansible-test-coverage/ansible_collections/ns/col/tests/integration/targets/hello/aliases --- old/ansible_core-2.20.10/test/integration/targets/ansible-test-coverage/ansible_collections/ns/col/tests/integration/targets/hello/aliases 1970-01-01 01:00:00.000000000 +0100 +++ new/ansible_core-2.20.11/test/integration/targets/ansible-test-coverage/ansible_collections/ns/col/tests/integration/targets/hello/aliases 2026-11-02 18:10:51.000000000 +0100 @@ -0,0 +1 @@ +context/controller diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/test/integration/targets/ansible-test-coverage/ansible_collections/ns/col/tests/integration/targets/hello/runme.sh new/ansible_core-2.20.11/test/integration/targets/ansible-test-coverage/ansible_collections/ns/col/tests/integration/targets/hello/runme.sh --- old/ansible_core-2.20.10/test/integration/targets/ansible-test-coverage/ansible_collections/ns/col/tests/integration/targets/hello/runme.sh 1970-01-01 01:00:00.000000000 +0100 +++ new/ansible_core-2.20.11/test/integration/targets/ansible-test-coverage/ansible_collections/ns/col/tests/integration/targets/hello/runme.sh 2026-11-02 18:10:51.000000000 +0100 @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -eu + +python.py world.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/test/integration/targets/ansible-test-coverage/ansible_collections/ns/col/tests/integration/targets/hello/world.py new/ansible_core-2.20.11/test/integration/targets/ansible-test-coverage/ansible_collections/ns/col/tests/integration/targets/hello/world.py --- old/ansible_core-2.20.10/test/integration/targets/ansible-test-coverage/ansible_collections/ns/col/tests/integration/targets/hello/world.py 1970-01-01 01:00:00.000000000 +0100 +++ new/ansible_core-2.20.11/test/integration/targets/ansible-test-coverage/ansible_collections/ns/col/tests/integration/targets/hello/world.py 2026-11-02 18:10:51.000000000 +0100 @@ -0,0 +1,3 @@ +from __future__ import annotations + +print("Hello World!") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/test/integration/targets/ansible-test-coverage/ansible_collections/ns/col/tests/unit/test_something.py new/ansible_core-2.20.11/test/integration/targets/ansible-test-coverage/ansible_collections/ns/col/tests/unit/test_something.py --- old/ansible_core-2.20.10/test/integration/targets/ansible-test-coverage/ansible_collections/ns/col/tests/unit/test_something.py 1970-01-01 01:00:00.000000000 +0100 +++ new/ansible_core-2.20.11/test/integration/targets/ansible-test-coverage/ansible_collections/ns/col/tests/unit/test_something.py 2026-11-02 18:10:51.000000000 +0100 @@ -0,0 +1,5 @@ +from __future__ import annotations + + +def test_me() -> None: + pass diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/test/integration/targets/ansible-test-coverage/runme.sh new/ansible_core-2.20.11/test/integration/targets/ansible-test-coverage/runme.sh --- old/ansible_core-2.20.10/test/integration/targets/ansible-test-coverage/runme.sh 2026-10-05 17:06:40.000000000 +0200 +++ new/ansible_core-2.20.11/test/integration/targets/ansible-test-coverage/runme.sh 2026-11-02 18:10:51.000000000 +0100 @@ -7,10 +7,31 @@ # common args for all tests common=(--venv --color --truncate 0 "${@}") -# run a lightweight test that generates code coverge output -ansible-test sanity --test import "${common[@]}" --coverage +# run a lightweight test that generates code coverage output +ansible-test sanity --test import "${common[@]}" --coverage --python "${ANSIBLE_TEST_PYTHON_VERSION}" + +# run an integration test that generates code coverage for a Python file within the integration test +ansible-test integration "${common[@]}" --coverage + +# run a unit test that generates code coverage using a venv +ansible-test units "${common[@]}" --coverage --python "${ANSIBLE_TEST_PYTHON_VERSION}" --venv # report on code coverage in all supported formats -ansible-test coverage report "${common[@]}" +ansible-test coverage report "${common[@]}" | tee coverage.report ansible-test coverage html "${common[@]}" ansible-test coverage xml "${common[@]}" + +# ensure import test coverage was collected +grep '^plugins/module_utils/test_util.py .* 100%$' coverage.report + +# ensure integration test coverage was collected +grep '^tests/integration/targets/hello/world.py .* 100%$' coverage.report + +# ensure unit test coverage was collected +grep '^tests/unit/test_something.py .* 100%$' coverage.report + +# ensure tests/output/ (from --venv) does not appear in the coverage report (except in the report filename) +if grep "^tests/output/" coverage.report; then + echo "unexpected coverage output: tests/output/" + exit 1 +fi diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/test/integration/targets/become_sudo/files/sudoshim.sh new/ansible_core-2.20.11/test/integration/targets/become_sudo/files/sudoshim.sh --- old/ansible_core-2.20.10/test/integration/targets/become_sudo/files/sudoshim.sh 2026-10-05 17:06:40.000000000 +0200 +++ new/ansible_core-2.20.11/test/integration/targets/become_sudo/files/sudoshim.sh 2026-11-02 18:10:51.000000000 +0100 @@ -11,6 +11,9 @@ "--intermediate-user") intermediate_user_idx="${i}" ;; + "--set-lang") + lang_idx="${i}" + ;; esac done @@ -19,4 +22,11 @@ unset "args[intermediate_user_idx]" unset "args[intermediate_user_idx+1]" +if [ -n "${lang_idx-}" ]; then + export LANG="${args[lang_idx+1]}" + export LC_ALL="${args[lang_idx+1]}" + unset "args[lang_idx]" + unset "args[lang_idx+1]" +fi + exec sudo -n -u "${intermediate_user_name}" sudo -k "${args[@]}" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/test/integration/targets/become_sudo/tasks/main.yml new/ansible_core-2.20.11/test/integration/targets/become_sudo/tasks/main.yml --- old/ansible_core-2.20.10/test/integration/targets/become_sudo/tasks/main.yml 2026-10-05 17:06:40.000000000 +0200 +++ new/ansible_core-2.20.11/test/integration/targets/become_sudo/tasks/main.yml 2026-11-02 18:10:51.000000000 +0100 @@ -82,3 +82,42 @@ that: - custom_flags.stdout.strip() == test_user_name - no_user.stdout.strip() == "root" + +- name: Test sudo with i18n, required for sudo-rs, only ubuntu + when: + - ansible_facts['system'] == 'Linux' + - ansible_facts['distribution'] == 'Ubuntu' + vars: + testlocales: + - ar_EG.UTF-8 # كَلِمَةُ الْسِّرِ + - fr_FR.UTF-8 # Mot de passe + - es_ES.UTF-8 # Contraseña + - de_DE.UTF-8 # Passwort + - ja_JP.UTF-8 # パスワード + - km_KH.UTF-8 # ពាក្យសម្ងាត់ + - th_TH.UTF-8 # Sหัสผ่าน + - tr_TR.UTF-8 # Şifre + - uk_UA.UTF-8 # Пароль + - vi_VN.UTF-8 # Mật khẩu + block: + - name: Setup the locales + become: yes + block: + - name: ensure debian locales package is available + apt: + name: locales + state: present + + - name: install language packs (also generates locales) + apt: + name: "{{ testlocales | map( 'regex_replace', '^(\\w{2})_.+', 'language-pack-\\1') | list }}" + + - name: Actually test sudo with locales + become_method: sudo + become_user: '{{ target_user_name }}' + become_exe: /tmp/sudoshim.sh + become_flags: '{{ default_become_flags }} {{ intermediate_flags }} --set-lang "{{item}}"' + vars: + ansible_become_password: '{{ intermediate_user_password }}' + shell: 'whoami && echo "${LANG} is lang and ${LC_ALL} is lc_all"' + loop: '{{ testlocales }}' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/test/lib/ansible_test/_data/completion/remote.txt new/ansible_core-2.20.11/test/lib/ansible_test/_data/completion/remote.txt --- old/ansible_core-2.20.10/test/lib/ansible_test/_data/completion/remote.txt 2026-10-05 17:06:40.000000000 +0200 +++ new/ansible_core-2.20.11/test/lib/ansible_test/_data/completion/remote.txt 2026-11-02 18:10:51.000000000 +0100 @@ -2,13 +2,12 @@ alpine become=doas_sudo provider=aws arch=x86_64 fedora/42 python=3.13 become=sudo provider=aws arch=x86_64 alias=fedora/latest fedora become=sudo provider=aws arch=x86_64 -freebsd/13.5 python=3.11 python_dir=/usr/local/bin become=su_sudo provider=aws arch=x86_64 alias=freebsd/13 -freebsd/14.3 python=3.11 python_dir=/usr/local/bin become=su_sudo provider=aws arch=x86_64 alias=freebsd/14,freebsd/latest +freebsd/14.5 python=3.14 python_dir=/usr/local/bin become=su_sudo provider=aws arch=x86_64 alias=freebsd/14,freebsd/latest freebsd python_dir=/usr/local/bin become=su_sudo provider=aws arch=x86_64 macos/15.3 python=3.13 python_dir=/usr/local/bin become=sudo provider=mac arch=aarch64 alias=macos/15,macos/latest macos python_dir=/usr/local/bin become=sudo provider=mac arch=aarch64 -rhel/9.7 python=3.9,3.12 become=sudo provider=aws arch=x86_64 alias=rhel/9 -rhel/10.1 python=3.12 become=sudo provider=aws arch=x86_64 alias=rhel/10,rhel/latest +rhel/9.8 python=3.9,3.14 become=sudo provider=aws arch=x86_64 alias=rhel/9 +rhel/10.2 python=3.12,3.14 become=sudo provider=aws arch=x86_64 alias=rhel/10,rhel/latest rhel become=sudo provider=aws arch=x86_64 ubuntu/22.04 python=3.10 become=sudo provider=aws arch=x86_64 ubuntu/24.04 python=3.12 become=sudo provider=aws arch=x86_64 alias=ubuntu/latest diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/test/lib/ansible_test/_internal/coverage_util.py new/ansible_core-2.20.11/test/lib/ansible_test/_internal/coverage_util.py --- old/ansible_core-2.20.10/test/lib/ansible_test/_internal/coverage_util.py 2026-10-05 17:06:40.000000000 +0200 +++ new/ansible_core-2.20.11/test/lib/ansible_test/_internal/coverage_util.py 2026-11-02 18:10:51.000000000 +0100 @@ -261,7 +261,7 @@ */pyshared/* */pytest */AnsiballZ_*.py - */test/results/* + */test/results/.tmp/delegation/* """ coverage_config = coverage_config.lstrip() @@ -279,8 +279,8 @@ ] omit_patterns = [ - # {base}/ansible_collections/{ns}/{col}/tests/output/* - os.path.join(data_context().content.root, data_context().content.results_path, '*'), + # {base}/ansible_collections/{ns}/{col}/tests/output/.tmp/delegation/* + os.path.join(data_context().content.root, data_context().content.results_path, '.tmp/delegation/*'), ] include = textwrap.indent('\n'.join(include_patterns), ' ' * 4) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/test/lib/ansible_test/_util/target/setup/bootstrap.sh new/ansible_core-2.20.11/test/lib/ansible_test/_util/target/setup/bootstrap.sh --- old/ansible_core-2.20.10/test/lib/ansible_test/_util/target/setup/bootstrap.sh 2026-10-05 17:06:40.000000000 +0200 +++ new/ansible_core-2.20.11/test/lib/ansible_test/_util/target/setup/bootstrap.sh 2026-11-02 18:10:51.000000000 +0100 @@ -199,6 +199,11 @@ # Declare platform/python version combinations which do not have supporting OS packages available. # For these combinations ansible-test will use pip to install the requirements instead. case "${platform_version}/${python_version}" in + "14.5/3.14") + jinja2_pkg="" # not available + cryptography_pkg="" # not available + pyyaml_pkg="" # not available + ;; *) # just assume nothing is available jinja2_pkg="" # not available @@ -314,10 +319,15 @@ bootstrap_remote_rhel_10() { - py_pkg_prefix="python3" + if [ "${python_version}" = "3.12" ]; then + py_pkg_prefix="python3" + else + py_pkg_prefix="python${python_version}" + fi packages=" gcc + python3-packaging ${py_pkg_prefix}-devel ${py_pkg_prefix}-pip " @@ -326,11 +336,19 @@ packages=" ${packages} ${py_pkg_prefix}-cryptography - ${py_pkg_prefix}-jinja2 - ${py_pkg_prefix}-packaging ${py_pkg_prefix}-pyyaml - ${py_pkg_prefix}-resolvelib " + + if [ "${python_version}" = "3.12" ]; then + # These packages are not available for all Python versions. + # Install them when possible, otherwise let ansible-test install them from PyPI. + packages=" + ${packages} + ${py_pkg_prefix}-jinja2 + ${py_pkg_prefix}-packaging + ${py_pkg_prefix}-resolvelib + " + fi fi retry_init diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/test/units/cli/galaxy/test_collection_extract_tar.py new/ansible_core-2.20.11/test/units/cli/galaxy/test_collection_extract_tar.py --- old/ansible_core-2.20.10/test/units/cli/galaxy/test_collection_extract_tar.py 2026-10-05 17:06:40.000000000 +0200 +++ new/ansible_core-2.20.11/test/units/cli/galaxy/test_collection_extract_tar.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import annotations - -import pytest - -from ansible.galaxy.collection import _extract_tar_dir - - [email protected] -def fake_tar_obj(mocker): - m_tarfile = mocker.Mock() - m_tarfile.type = mocker.Mock(return_value=b'99') - m_tarfile.SYMTYPE = mocker.Mock(return_value=b'22') - - return m_tarfile - - -def test_extract_tar_dir_exists(mocker, fake_tar_obj): - mocker.patch('os.makedirs', return_value=None) - m_makedir = mocker.patch('os.mkdir', return_value=None) - mocker.patch('os.path.isdir', return_value=True) - - _extract_tar_dir(fake_tar_obj, '/some/dir', b'/some/dest') - - assert not m_makedir.called - - -def test_extract_tar_dir_does_not_exist(mocker, fake_tar_obj): - mocker.patch('os.makedirs', return_value=None) - m_makedir = mocker.patch('os.mkdir', return_value=None) - mocker.patch('os.path.isdir', return_value=False) - - _extract_tar_dir(fake_tar_obj, '/some/dir', b'/some/dest') - - assert m_makedir.called - assert m_makedir.call_args[0] == (b'/some/dir', 0o0755) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.20.10/test/units/module_utils/urls/test_mask_url.py new/ansible_core-2.20.11/test/units/module_utils/urls/test_mask_url.py --- old/ansible_core-2.20.10/test/units/module_utils/urls/test_mask_url.py 2026-10-05 17:06:40.000000000 +0200 +++ new/ansible_core-2.20.11/test/units/module_utils/urls/test_mask_url.py 2026-11-02 18:10:51.000000000 +0100 @@ -36,3 +36,45 @@ for notmasked in wanted: assert notmasked in masked + + +_ipv6 = 'Invalid IPv6 URL' + + [email protected]( + 'url, expected', + ( + ('http://secretuser:secretpassword@badunicodeat.com:80/file.html?nothing=something', None), + ('http://secretuser:@badunicodeslash.com:443/file.html', None), + ('http://[email protected]:80', None), + ('http://:secretpassword@badunicodequestion.com:00/file.html?this=breaksparse', None), + ('https://[::1/index.html', _ipv6), + ('https://example.com]:443/index.html', _ipv6), + ('https://[fe80::1:8080/index.html', _ipv6), + ('//[::1/index.html', _ipv6), + ('https://secretuser:secretpass@[::1/index.html', _ipv6), + ('https://secretuser@[::1/index.html', _ipv6), + ('ftp://secretuser:secretpass@[fe80::1:8080/pub/file.txt', _ipv6), + ('https://:secretpass@::1]/index.html', _ipv6), + ('https://secretuser:secretpass@[::1', _ipv6), + # a scheme-relative url has no scheme to anchor on + ('//secretuser:secretpass@[::1/index.html', _ipv6), + # only `/` ends the authority, so a `?` or `#` cannot hide the userinfo + ('https://[::1secretuser:sec?retpass@host/index.html', _ipv6), + ('https://secretuser:secretpass@[::1#fragment', _ipv6), + # the last `@` separates userinfo from host, matching how urlparse splits it + ('https://secretuser:secret@pass@[::1/index.html', _ipv6), + # combine! + ('http://:secretpassword@[::1/:01/badunicodeat/file.html?this=breaksparse', None), + ) +) +def test_mask_url_exceptions(url, expected): + with pytest.raises(ValueError, match="^(?!.*secret).*$") as e: + mask_url(url) + + msg = str(e) + if expected is None: + assert "ValueError('mask_url could not parse the url provided')" in msg + else: + assert 'mask_url could not parse the url provided' in msg + assert expected in msg ++++++ ansible_core-2.20.10.tar.gz.sha256 -> ansible_core-2.20.11.tar.gz.sha256 ++++++ --- /work/SRC/openSUSE:Factory/ansible-core-2.20/ansible_core-2.20.10.tar.gz.sha256 2026-10-08 13:28:35.173801227 +0200 +++ /work/SRC/openSUSE:Factory/.ansible-core-2.20.new.3759290/ansible_core-2.20.11.tar.gz.sha256 2026-11-06 20:00:51.150404387 +0100 @@ -1 +1 @@ -577b8d3c18e4228310a4e5180c3e133bb3955b378367077bfc643f3c21ebee5c ansible_core-2.20.10.tar.gz +f1738b566514c36e4cc635d10e4d728555d55867a9bdbfed709f05aa6bab4d82 ansible_core-2.20.11.tar.gz
