Version in base suite: 2.19.4-0+deb13u1 Base version: ansible-core_2.19.4-0+deb13u1 Target version: ansible-core_2.19.11-0+deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/a/ansible-core/ansible-core_2.19.4-0+deb13u1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/a/ansible-core/ansible-core_2.19.11-0+deb13u1.dsc PKG-INFO | 2 ansible_core.egg-info/PKG-INFO | 2 ansible_core.egg-info/SOURCES.txt | 21 changelogs/CHANGELOG-v2.19.rst | 150 ++ changelogs/changelog.yaml | 255 +++ debian/changelog | 71 debian/salsa-ci.yml | 4 lib/ansible/_internal/_yaml/_loader.py | 17 lib/ansible/cli/galaxy.py | 6 lib/ansible/config/base.yml | 13 lib/ansible/executor/module_common.py | 10 lib/ansible/galaxy/collection/concrete_artifact_manager.py | 66 lib/ansible/galaxy/collection/galaxy_api_proxy.py | 7 lib/ansible/module_utils/ansible_release.py | 2 lib/ansible/module_utils/basic.py | 16 lib/ansible/module_utils/common/parameters.py | 16 lib/ansible/module_utils/common/text/formatters.py | 2 lib/ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1 | 2 lib/ansible/modules/get_url.py | 8 lib/ansible/plugins/action/__init__.py | 4 lib/ansible/plugins/become/__init__.py | 2 lib/ansible/plugins/connection/_paramiko_ssh.py | 713 ++++++++++ lib/ansible/plugins/connection/local.py | 4 lib/ansible/plugins/connection/paramiko_ssh.py | 4 lib/ansible/plugins/connection/psrp.py | 9 lib/ansible/plugins/connection/winrm.py | 32 lib/ansible/plugins/lookup/config.py | 7 lib/ansible/plugins/lookup/first_found.py | 2 lib/ansible/plugins/vars/host_group_vars.py | 7 lib/ansible/release.py | 2 lib/ansible/utils/galaxy.py | 2 lib/ansible/utils/jsonrpc.py | 4 lib/ansible/utils/vars.py | 4 lib/ansible/vars/manager.py | 14 pyproject.toml | 2 test/integration/targets/ansible-galaxy-role/tasks/git-config-injection.yml | 52 test/integration/targets/ansible-galaxy-role/tasks/main.yml | 1 test/integration/targets/ansible-test-cloud-azure/aliases | 1 test/integration/targets/ansible-test-metadata/aliases | 3 test/integration/targets/ansible-test-metadata/runme.sh | 9 test/integration/targets/ansible-test-sanity-runtime-metadata/ansible_collections/ns/prerelease/galaxy.yml | 5 test/integration/targets/ansible-test-sanity-runtime-metadata/ansible_collections/ns/prerelease/meta/runtime.yml | 25 test/integration/targets/ansible-test-sanity-runtime-metadata/expected-prerelease.txt | 1 test/integration/targets/ansible-test-sanity-runtime-metadata/runme.sh | 5 test/integration/targets/become_su/aliases | 1 test/integration/targets/become_su/tasks/main.yml | 43 test/integration/targets/deprecations/injectfacts.yml | 8 test/integration/targets/deprecations/runme.sh | 6 test/integration/targets/gathering_facts/runme.sh | 3 test/integration/targets/gathering_facts/test_module_defaults.yml | 18 test/integration/targets/get_url/tasks/main.yml | 101 + test/integration/targets/iptables/tasks/main.yml | 10 test/integration/targets/lookup_config/tasks/main.yml | 5 test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/meta/runtime.yml | 2 test/integration/targets/module_defaults/test_action_groups.yml | 15 test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/library/add_type_test.ps1 | 1 test/integration/targets/no_log/library/module.py | 2 test/integration/targets/no_log/runme.sh | 3 test/integration/targets/no_log/secretvars.yml | 3 test/integration/targets/no_log/sub_masking.yml | 60 test/integration/targets/package/tasks/main.yml | 37 test/integration/targets/pyyaml/runme.sh | 10 test/integration/targets/pyyaml/runme.yml | 9 test/integration/targets/rpm_key/tasks/main.yaml | 4 test/integration/targets/service/tasks/tests.yml | 6 test/integration/targets/uri/tasks/main.yml | 2 test/integration/targets/var_blending/error_handling.yml | 14 test/integration/targets/var_blending/runme.sh | 9 test/integration/targets/var_blending/supersecretvaultsecret | 1 test/integration/targets/var_blending/vars/bad_vault.yml | 10 test/integration/targets/win_app_control/setup.yml | 2 test/lib/ansible_test/_data/completion/docker.txt | 6 test/lib/ansible_test/_data/completion/remote.txt | 18 test/lib/ansible_test/_data/completion/windows.txt | 2 test/lib/ansible_test/_internal/ansible_util.py | 36 test/lib/ansible_test/_internal/ci/azp.py | 40 test/lib/ansible_test/_internal/ci/gha.py | 106 + test/lib/ansible_test/_internal/cli/compat.py | 36 test/lib/ansible_test/_internal/cli/environments.py | 6 test/lib/ansible_test/_internal/commands/integration/__init__.py | 2 test/lib/ansible_test/_internal/completion.py | 46 test/lib/ansible_test/_internal/constants.py | 2 test/lib/ansible_test/_internal/core_ci.py | 2 test/lib/ansible_test/_internal/host_configs.py | 2 test/lib/ansible_test/_util/controller/sanity/code-smell/runtime-metadata.py | 2 test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/schema.py | 5 test/lib/ansible_test/_util/target/setup/bootstrap.sh | 8 test/sanity/code-smell/no-s3.json | 4 test/sanity/code-smell/no-s3.py | 27 test/units/ansible_test/_internal/ci/test_azp.py | 96 + test/units/module_utils/basic/test_human_to_bytes.py | 34 test/units/module_utils/basic/test_run_command.py | 78 + test/units/modules/test_get_url.py | 46 test/units/playbook/test_task.py | 12 test/units/plugins/become/test_su.py | 2 test/units/plugins/connection/test_winrm.py | 6 test/units/utils/test_jsonrpc.py | 31 97 files changed, 2412 insertions(+), 212 deletions(-) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp1z_4rs2j/ansible-core_2.19.4-0+deb13u1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp1z_4rs2j/ansible-core_2.19.11-0+deb13u1.dsc: no acceptable signature found diff -Nru ansible-core-2.19.4/PKG-INFO ansible-core-2.19.11/PKG-INFO --- ansible-core-2.19.4/PKG-INFO 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/PKG-INFO 2026-06-18 19:34:02.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: ansible-core -Version: 2.19.4 +Version: 2.19.11 Summary: Radically simple IT automation Author: Ansible Project Project-URL: Homepage, https://ansible.com/ diff -Nru ansible-core-2.19.4/ansible_core.egg-info/PKG-INFO ansible-core-2.19.11/ansible_core.egg-info/PKG-INFO --- ansible-core-2.19.4/ansible_core.egg-info/PKG-INFO 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/ansible_core.egg-info/PKG-INFO 2026-06-18 19:34:02.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: ansible-core -Version: 2.19.4 +Version: 2.19.11 Summary: Radically simple IT automation Author: Ansible Project Project-URL: Homepage, https://ansible.com/ diff -Nru ansible-core-2.19.4/ansible_core.egg-info/SOURCES.txt ansible-core-2.19.11/ansible_core.egg-info/SOURCES.txt --- ansible-core-2.19.4/ansible_core.egg-info/SOURCES.txt 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/ansible_core.egg-info/SOURCES.txt 2026-06-18 19:34:02.000000000 +0000 @@ -562,6 +562,7 @@ lib/ansible/plugins/callback/tree.py lib/ansible/plugins/cliconf/__init__.py lib/ansible/plugins/connection/__init__.py +lib/ansible/plugins/connection/_paramiko_ssh.py lib/ansible/plugins/connection/local.py lib/ansible/plugins/connection/paramiko_ssh.py lib/ansible/plugins/connection/psrp.py @@ -1020,6 +1021,7 @@ test/integration/targets/ansible-galaxy-role/files/safe-symlinks/tasks/utils/suite.yml test/integration/targets/ansible-galaxy-role/meta/main.yml test/integration/targets/ansible-galaxy-role/tasks/dir-traversal.yml +test/integration/targets/ansible-galaxy-role/tasks/git-config-injection.yml test/integration/targets/ansible-galaxy-role/tasks/main.yml test/integration/targets/ansible-galaxy-role/tasks/valid-role-symlinks.yml test/integration/targets/ansible-galaxy/files/testserver.py @@ -1167,6 +1169,8 @@ test/integration/targets/ansible-test-integration/ansible_collections/ns/col/plugins/modules/hello.py test/integration/targets/ansible-test-integration/ansible_collections/ns/col/tests/integration/targets/hello/aliases test/integration/targets/ansible-test-integration/ansible_collections/ns/col/tests/integration/targets/hello/tasks/main.yml +test/integration/targets/ansible-test-metadata/aliases +test/integration/targets/ansible-test-metadata/runme.sh test/integration/targets/ansible-test-no-tty/aliases test/integration/targets/ansible-test-no-tty/runme.sh test/integration/targets/ansible-test-no-tty/ansible_collections/ns/col/run-with-pty.py @@ -1220,10 +1224,13 @@ test/integration/targets/ansible-test-sanity-replace-urlopen/ansible_collections/ns/col/plugins/modules/check-me.py test/integration/targets/ansible-test-sanity-runtime-metadata/aliases test/integration/targets/ansible-test-sanity-runtime-metadata/expected-no_version.txt +test/integration/targets/ansible-test-sanity-runtime-metadata/expected-prerelease.txt test/integration/targets/ansible-test-sanity-runtime-metadata/expected-version.txt test/integration/targets/ansible-test-sanity-runtime-metadata/runme.sh test/integration/targets/ansible-test-sanity-runtime-metadata/ansible_collections/ns/no_version/galaxy.yml test/integration/targets/ansible-test-sanity-runtime-metadata/ansible_collections/ns/no_version/meta/runtime.yml +test/integration/targets/ansible-test-sanity-runtime-metadata/ansible_collections/ns/prerelease/galaxy.yml +test/integration/targets/ansible-test-sanity-runtime-metadata/ansible_collections/ns/prerelease/meta/runtime.yml test/integration/targets/ansible-test-sanity-runtime-metadata/ansible_collections/ns/version/galaxy.yml test/integration/targets/ansible-test-sanity-runtime-metadata/ansible_collections/ns/version/meta/runtime.yml test/integration/targets/ansible-test-sanity-shebang/aliases @@ -2022,6 +2029,7 @@ test/integration/targets/deprecations/entry_key_deprecated.cfg test/integration/targets/deprecations/entry_key_deprecated2.cfg test/integration/targets/deprecations/entry_key_not_deprecated.cfg +test/integration/targets/deprecations/injectfacts.yml test/integration/targets/deprecations/runme.sh test/integration/targets/deprecations/cache_plugins/notjsonfile.py test/integration/targets/deprecations/collections/ansible_collections/foo/bar/plugins/__init__.py @@ -3290,6 +3298,7 @@ test/integration/targets/no_log/no_log_suboptions_invalid.yml test/integration/targets/no_log/runme.sh test/integration/targets/no_log/secretvars.yml +test/integration/targets/no_log/sub_masking.yml test/integration/targets/no_log/action_plugins/action_sets_no_log.py test/integration/targets/no_log/library/module.py test/integration/targets/noexec/aliases @@ -3479,6 +3488,7 @@ test/integration/targets/python_module_rlimit_nofile/tasks/main.yml test/integration/targets/pyyaml/aliases test/integration/targets/pyyaml/runme.sh +test/integration/targets/pyyaml/runme.yml test/integration/targets/raw/aliases test/integration/targets/raw/runme.sh test/integration/targets/raw/runme.yml @@ -4183,13 +4193,15 @@ test/integration/targets/user/tasks/test_umask.yml test/integration/targets/user/vars/main.yml test/integration/targets/var_blending/aliases +test/integration/targets/var_blending/error_handling.yml test/integration/targets/var_blending/inventory test/integration/targets/var_blending/runme.sh +test/integration/targets/var_blending/supersecretvaultsecret test/integration/targets/var_blending/test_var_blending.yml test/integration/targets/var_blending/test_vars.yml test/integration/targets/var_blending/vars_file.yml test/integration/targets/var_blending/group_vars/all -test/integration/targets/var_blending/group_vars/local +test/integration/targets/var_blending/group_vars/local/main.yml test/integration/targets/var_blending/host_vars/testhost test/integration/targets/var_blending/roles/test_var_blending/defaults/main.yml test/integration/targets/var_blending/roles/test_var_blending/files/foo.txt @@ -4197,6 +4209,7 @@ test/integration/targets/var_blending/roles/test_var_blending/templates/foo.j2 test/integration/targets/var_blending/roles/test_var_blending/vars/main.yml test/integration/targets/var_blending/roles/test_var_blending/vars/more_vars.yml +test/integration/targets/var_blending/vars/bad_vault.yml test/integration/targets/var_inheritance/aliases test/integration/targets/var_inheritance/tasks/main.yml test/integration/targets/var_precedence/aliases @@ -4473,6 +4486,7 @@ test/lib/ansible_test/_internal/venv.py test/lib/ansible_test/_internal/ci/__init__.py test/lib/ansible_test/_internal/ci/azp.py +test/lib/ansible_test/_internal/ci/gha.py test/lib/ansible_test/_internal/ci/local.py test/lib/ansible_test/_internal/classification/__init__.py test/lib/ansible_test/_internal/classification/common.py @@ -4693,6 +4707,8 @@ test/sanity/code-smell/mypy.py test/sanity/code-smell/mypy.requirements.in test/sanity/code-smell/mypy.requirements.txt +test/sanity/code-smell/no-s3.json +test/sanity/code-smell/no-s3.py test/sanity/code-smell/no-unwanted-characters.json test/sanity/code-smell/no-unwanted-characters.py test/sanity/code-smell/no-unwanted-files.json @@ -4861,6 +4877,7 @@ test/units/ansible_test/test_diff.py test/units/ansible_test/_internal/__init__.py test/units/ansible_test/_internal/test_util.py +test/units/ansible_test/_internal/ci/test_azp.py test/units/ansible_test/diff/add_binary_file.diff test/units/ansible_test/diff/add_text_file.diff test/units/ansible_test/diff/add_trailing_newline.diff @@ -5012,6 +5029,7 @@ test/units/module_utils/basic/test_get_file_attributes.py test/units/module_utils/basic/test_get_module_path.py test/units/module_utils/basic/test_heuristic_log_sanitize.py +test/units/module_utils/basic/test_human_to_bytes.py test/units/module_utils/basic/test_imports.py test/units/module_utils/basic/test_log.py test/units/module_utils/basic/test_no_log.py @@ -5378,6 +5396,7 @@ test/units/utils/test_helpers.py test/units/utils/test_isidentifier.py test/units/utils/test_json.py +test/units/utils/test_jsonrpc.py test/units/utils/test_listify.py test/units/utils/test_plugin_docs.py test/units/utils/test_serialization.py diff -Nru ansible-core-2.19.4/changelogs/CHANGELOG-v2.19.rst ansible-core-2.19.11/changelogs/CHANGELOG-v2.19.rst --- ansible-core-2.19.4/changelogs/CHANGELOG-v2.19.rst 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/changelogs/CHANGELOG-v2.19.rst 2026-06-18 19:34:02.000000000 +0000 @@ -4,6 +4,156 @@ .. contents:: Topics +v2.19.11 +======== + +Release Summary +--------------- + +| Release Date: 2026-06-18 +| `Porting Guide `__ + +Minor Changes +------------- + +- ansible-test - Replace FreeBSD 14.2 with 14.3. + +Security Fixes +-------------- + +- ansible-galaxy install - Ensure role requirements are passed as positional arguments to :command:`git clone`. Previously, a malicious role author could inject arbitrary git configuration in role dependencies. (CVE-2026-11332) + +Bugfixes +-------- + +- module_utils sanitize_keys and remove_value functions now sort their input to ensure matching subsets are always obscured. + +v2.19.10 +======== + +Release Summary +--------------- + +| Release Date: 2026-05-18 +| `Porting Guide `__ + +Security Fixes +-------------- + +- psrp - Do not log raw stdout/stderr on verbosity 5 when task has ``no_log: true`` set +- winrm - Do not log raw stdout/stderr on verbosity 5 when task has ``no_log: true`` set + +Bugfixes +-------- + +- ansible-test remote alias - Alias values for ``--controller`` and ``--target`` are properly resolved for ``remote``. Previously, remote alias values (e.g. ``fedora/latest``) resolved to the correct name only for the legacy ``--remote`` arg, failing with an unknown image error for the newer args. +- module_utils/basic.py - Fix ``AnsibleModule.run_command()`` to handle ``None`` return from non-blocking pipe reads (https://github.com/ansible/ansible/issues/86920). + +v2.19.9 +======= + +Release Summary +--------------- + +| Release Date: 2026-04-20 +| `Porting Guide `__ + +Minor Changes +------------- + +- ansible-test - Generate ``dist_info`` when running tests. +- ansible-test - Replace the ``parallels`` managed macOS provider with a new ``mac`` provider. +- ansible-test - Switch managed macOS remotes from x86_64 to aarch64. + +Bugfixes +-------- + +- ansible-galaxy collection - Fix using the server configuration for ``validate_certs`` when downloading collections. (https://github.com/ansible/ansible/issues/86694) + +v2.19.8 +======= + +Release Summary +--------------- + +| Release Date: 2026-03-23 +| `Porting Guide `__ + +Minor Changes +------------- + +- ansible-test - Add container/remote aliases for more loosely specifying managed test environments. +- ansible-test - Add support for using the Ansible Core CI service from GitHub Actions. + +Bugfixes +-------- + +- ansible-connection - Prevent unpickling failures in module contexts by ensuring that AnsibleTaggedObjects in pickled responses are converted to plain types in ``JsonRpcServer``. +- config lookup now uses preexisting constants for templating when needed. +- yaml loading - Fix traceback when parsing YAML strings (not files) when using the pure Python implementation of PyYAML. + +v2.19.7 +======= + +Release Summary +--------------- + +| Release Date: 2026-02-23 +| `Porting Guide `__ + +Minor Changes +------------- + +- ansible-test - Update URL used to download FreeBSD wheels for managed remotes. +- ansible-test - Use the new API endpoint for the Ansible Core CI service. + +Bugfixes +-------- + +- Fix up the Action plugin ``_make_tmp_path`` error to only include the command run rather than the shell's dataclass repr from ``mkdtemp``. +- local connection - Pass correct type to become plugins when checking password (https://github.com/ansible/ansible/issues/86458) + +v2.19.6 +======= + +Release Summary +--------------- + +| Release Date: 2026-01-29 +| `Porting Guide `__ + +Minor Changes +------------- + +- ansible-test - Replace RHEL 10.0 remote with 10.1. +- ansible-test - Replace RHEL 9.5 remote with 9.7. + +Bugfixes +-------- + +- Fix Windows LIB env var corruption (https://github.com/ansible-collections/ansible.windows/issues/297). +- ansible_local will no longer trigger variable injection default value deprecation. +- package, service, gather_facts - fix templating module_defaults for modules executed by these action plugins. (https://github.com/ansible/ansible/issues/85848) +- winrm - Provide a better error message if a domain user is specified using a User Principal Name (``UPN``) but the ``pykerberos`` library is not installed so Kerberos is unavailable. + +v2.19.5 +======= + +Release Summary +--------------- + +| Release Date: 2025-12-09 +| `Porting Guide `__ + +Bugfixes +-------- + +- Fix ``AnsibleModule.human_to_bytes()``, which was never adjusted after the standalone ``human_to_bytes()`` got a new parameter ``default_unit`` (https://github.com/ansible/ansible/pull/85259). +- Variable loading now uses file source instead of variables when invalidly formmated vars file is loaded. +- ansible-test - The runtime-metadata sanity test now ignores pre-release and build identifiers in collection versions. This prevents errors if a tombstone version is ``X.0.0``, while the collection's version is ``X.0.0-prerelease`` (https://github.com/ansible/ansible/issues/85193)." +- first_found - Correct the "Include tasks only if one of the files exists, otherwise skip" example. +- get_url - fix regex for GNU Digest line which is used in comparing checksums (https://github.com/ansible/ansible/issues/86132). + v2.19.4 ======= diff -Nru ansible-core-2.19.4/changelogs/changelog.yaml ansible-core-2.19.11/changelogs/changelog.yaml --- ansible-core-2.19.4/changelogs/changelog.yaml 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/changelogs/changelog.yaml 2026-06-18 19:34:02.000000000 +0000 @@ -1209,6 +1209,77 @@ fragments: - 2.19.1_summary.yaml release_date: '2025-08-25' + 2.19.10: + changes: + release_summary: '| Release Date: 2026-05-18 + + | `Porting Guide `__ + + ' + codename: What Is and What Should Never Be + fragments: + - 2.19.10_summary.yaml + release_date: '2026-05-18' + 2.19.10rc1: + changes: + bugfixes: + - ansible-test remote alias - Alias values for ``--controller`` and ``--target`` + are properly resolved for ``remote``. Previously, remote alias values (e.g. + ``fedora/latest``) resolved to the correct name only for the legacy ``--remote`` + arg, failing with an unknown image error for the newer args. + - module_utils/basic.py - Fix ``AnsibleModule.run_command()`` to handle ``None`` + return from non-blocking pipe reads (https://github.com/ansible/ansible/issues/86920). + release_summary: '| Release Date: 2026-05-11 + + | `Porting Guide `__ + + ' + security_fixes: + - 'psrp - Do not log raw stdout/stderr on verbosity 5 when task has ``no_log: + true`` set' + - 'winrm - Do not log raw stdout/stderr on verbosity 5 when task has ``no_log: + true`` set' + codename: What Is and What Should Never Be + fragments: + - 2.19.10rc1_summary.yaml + - 86920-fix-run-command-none-read.yml + - core_ci_remote_alias.yml + - winrm-psrp-nolog.yml + release_date: '2026-05-11' + 2.19.11: + changes: + release_summary: '| Release Date: 2026-06-18 + + | `Porting Guide `__ + + ' + codename: What Is and What Should Never Be + fragments: + - 2.19.11_summary.yaml + release_date: '2026-06-18' + 2.19.11rc1: + changes: + bugfixes: + - module_utils sanitize_keys and remove_value functions now sort their input + to ensure matching subsets are always obscured. + minor_changes: + - ansible-test - Replace FreeBSD 14.2 with 14.3. + release_summary: '| Release Date: 2026-06-11 + + | `Porting Guide `__ + + ' + security_fixes: + - ansible-galaxy install - Ensure role requirements are passed as positional + arguments to :command:`git clone`. Previously, a malicious role author could + inject arbitrary git configuration in role dependencies. (CVE-2026-11332) + codename: What Is and What Should Never Be + fragments: + - 2.19.11rc1_summary.yaml + - ansible-test-freebsd-14.3.yml + - fix-cloning-malformed-role-requirements.yml + - sort_obfuscation.yml + release_date: '2026-06-11' 2.19.1rc1: changes: bugfixes: @@ -1408,3 +1479,187 @@ - option_deprecation_help.yml - package_facts.yml release_date: '2025-10-29' + 2.19.5: + changes: + release_summary: '| Release Date: 2025-12-09 + + | `Porting Guide `__ + + ' + codename: What Is and What Should Never Be + fragments: + - 2.19.5_summary.yaml + release_date: '2025-12-09' + 2.19.5rc1: + changes: + bugfixes: + - Fix ``AnsibleModule.human_to_bytes()``, which was never adjusted after the + standalone ``human_to_bytes()`` got a new parameter ``default_unit`` (https://github.com/ansible/ansible/pull/85259). + - Variable loading now uses file source instead of variables when invalidly + formmated vars file is loaded. + - ansible-test - The runtime-metadata sanity test now ignores pre-release and + build identifiers in collection versions. This prevents errors if a tombstone + version is ``X.0.0``, while the collection's version is ``X.0.0-prerelease`` + (https://github.com/ansible/ansible/issues/85193)." + - first_found - Correct the "Include tasks only if one of the files exists, + otherwise skip" example. + - get_url - fix regex for GNU Digest line which is used in comparing checksums + (https://github.com/ansible/ansible/issues/86132). + release_summary: '| Release Date: 2025-12-02 + + | `Porting Guide `__ + + ' + codename: What Is and What Should Never Be + fragments: + - 2.19.5rc1_summary.yaml + - 85193-runtime-metadata.yml + - 85259-fix-human_to_bytes.yml + - first-found-example.yml + - get_url_regex.yml + - varloaderror.yml + release_date: '2025-12-02' + 2.19.6: + changes: + release_summary: '| Release Date: 2026-01-29 + + | `Porting Guide `__ + + ' + codename: What Is and What Should Never Be + fragments: + - 2.19.6_summary.yaml + release_date: '2026-01-29' + 2.19.6rc1: + changes: + bugfixes: + - Fix Windows LIB env var corruption (https://github.com/ansible-collections/ansible.windows/issues/297). + - ansible_local will no longer trigger variable injection default value deprecation. + - package, service, gather_facts - fix templating module_defaults for modules + executed by these action plugins. (https://github.com/ansible/ansible/issues/85848) + - winrm - Provide a better error message if a domain user is specified using + a User Principal Name (``UPN``) but the ``pykerberos`` library is not installed + so Kerberos is unavailable. + minor_changes: + - ansible-test - Replace RHEL 10.0 remote with 10.1. + - ansible-test - Replace RHEL 9.5 remote with 9.7. + release_summary: '| Release Date: 2026-01-23 + + | `Porting Guide `__ + + ' + codename: What Is and What Should Never Be + fragments: + - 2.19.6rc1_summary.yaml + - ansible-test-rhel-10.1.yml + - ansible_local_nodepr.yml + - fix-windows-lib-env-corruption.yml + - module_defaults-action-plugin-templating.yml + - winrm-kerberos.yml + release_date: '2026-01-22' + 2.19.7: + changes: + release_summary: '| Release Date: 2026-02-23 + + | `Porting Guide `__ + + ' + codename: What Is and What Should Never Be + fragments: + - 2.19.7_summary.yaml + release_date: '2026-02-23' + 2.19.7rc1: + changes: + bugfixes: + - Fix up the Action plugin ``_make_tmp_path`` error to only include the command + run rather than the shell's dataclass repr from ``mkdtemp``. + - local connection - Pass correct type to become plugins when checking password + (https://github.com/ansible/ansible/issues/86458) + minor_changes: + - ansible-test - Update URL used to download FreeBSD wheels for managed remotes. + - ansible-test - Use the new API endpoint for the Ansible Core CI service. + release_summary: '| Release Date: 2026-02-17 + + | `Porting Guide `__ + + ' + codename: What Is and What Should Never Be + fragments: + - 2.19.7rc1_summary.yaml + - 86473_su_become_local.yml + - ansible-test-api-endpoint.yml + - ansible-test-spare-tire.yml + - make-tmp-path-msg.yml + release_date: '2026-02-17' + 2.19.8: + changes: + release_summary: '| Release Date: 2026-03-23 + + | `Porting Guide `__ + + ' + codename: What Is and What Should Never Be + fragments: + - 2.19.8_summary.yaml + release_date: '2026-03-23' + 2.19.8rc1: + changes: + bugfixes: + - ansible-connection - Prevent unpickling failures in module contexts by ensuring + that AnsibleTaggedObjects in pickled responses are converted to plain types + in ``JsonRpcServer``. + - config lookup now uses preexisting constants for templating when needed. + - yaml loading - Fix traceback when parsing YAML strings (not files) when using + the pure Python implementation of PyYAML. + minor_changes: + - ansible-test - Add container/remote aliases for more loosely specifying managed + test environments. + - ansible-test - Add support for using the Ansible Core CI service from GitHub + Actions. + release_summary: '| Release Date: 2026-03-16 + + | `Porting Guide `__ + + ' + codename: What Is and What Should Never Be + fragments: + - 2.19.8rc1_summary.yaml + - 86601-untag-jsonrpc-responses.yml + - ansible-test-completion-aliases.yml + - ansible-test-github-actions.yml + - configlookupfix.yml + - pyyaml-name.yml + release_date: '2026-03-16' + 2.19.9: + changes: + release_summary: '| Release Date: 2026-04-20 + + | `Porting Guide `__ + + ' + codename: What Is and What Should Never Be + fragments: + - 2.19.9_summary.yaml + release_date: '2026-04-20' + 2.19.9rc1: + changes: + bugfixes: + - ansible-galaxy collection - Fix using the server configuration for ``validate_certs`` + when downloading collections. (https://github.com/ansible/ansible/issues/86694) + minor_changes: + - ansible-test - Generate ``dist_info`` when running tests. + - ansible-test - Replace the ``parallels`` managed macOS provider with a new + ``mac`` provider. + - ansible-test - Switch managed macOS remotes from x86_64 to aarch64. + release_summary: '| Release Date: 2026-04-13 + + | `Porting Guide `__ + + ' + codename: What Is and What Should Never Be + fragments: + - 2.19.9rc1_summary.yaml + - ansible-galaxy-validate_certs-server-config.yml + - ansible-test-dist-info.yml + - ansible-test-macos-aarch64.yml + release_date: '2026-04-13' diff -Nru ansible-core-2.19.4/debian/changelog ansible-core-2.19.11/debian/changelog --- ansible-core-2.19.4/debian/changelog 2025-11-07 22:26:04.000000000 +0000 +++ ansible-core-2.19.11/debian/changelog 2026-06-19 16:56:20.000000000 +0000 @@ -1,3 +1,74 @@ +ansible-core (2.19.11-0+deb13u1) trixie; urgency=medium + + * d/salsa-ci.yml: Run lintian CI against trixie for this branch New upstream + * New upstream version 2.19.5 + - Fix ``AnsibleModule.human_to_bytes()``, which was never adjusted after the + standalone ``human_to_bytes()`` got a new parameter ``default_unit`` + (https://github.com/ansible/ansible/pull/85259). + - Variable loading now uses file source instead of variables when invalidly + formmated vars file is loaded. + - ansible-test - The runtime-metadata sanity test now ignores pre-release + and build identifiers in collection versions. This prevents errors if a + tombstone version is ``X.0.0``, while the collection's version is + ``X.0.0-prerelease`` (https://github.com/ansible/ansible/issues/85193)." + - first_found - Correct the "Include tasks only if one of the files exists, + otherwise skip" example. + - get_url - fix regex for GNU Digest line which is used in comparing + checksums (https://github.com/ansible/ansible/issues/86132). + * New upstream version 2.19.6 + - ansible-test - Replace RHEL 10.0 remote with 10.1. + - ansible-test - Replace RHEL 9.5 remote with 9.7. + - Fix Windows LIB env var corruption. + - ansible_local will no longer trigger variable injection default value + deprecation. + - package, service, gather_facts - fix templating module_defaults for + modules executed by these action plugins. + - winrm - Provide a better error message if a domain user is specified using + a User Principal Name (UPN) but the pykerberos library is not installed so + Kerberos is unavailable. + * New upstream version 2.19.7 + - ansible-test - Update URL used to download FreeBSD wheels for managed + remotes. + - ansible-test - Use the new API endpoint for the Ansible Core CI service. + - Fix up the Action plugin _make_tmp_path error to only include the command + run rather than the shell's dataclass repr from mkdtemp. + - local connection - Pass correct type to become plugins when checking + password + * New upstream version 2.19.8 + - ansible-test - Add container/remote aliases for more loosely specifying + managed test environments. + - ansible-test - Add support for using the Ansible Core CI service from + GitHub Actions. + * New upstream version 2.19.9 + - ansible-test - Generate dist_info when running tests. + - ansible-test - Replace the parallels managed macOS provider with a new mac + provider. + - ansible-test - Switch managed macOS remotes from x86_64 to aarch64. + - ansible-galaxy collection - Fix using the server configuration for + validate_certs when downloading collections. + * New upstream version 2.19.10 + - psrp - Do not log raw stdout/stderr on verbosity 5 when task has + "no_log: true" set + - winrm - Do not log raw stdout/stderr on verbosity 5 when task has + "no_log: true" set + - ansible-test remote alias - Alias values for `--controller` and + `--target` are properly resolved for `remote`. Previously, remote + alias values (e.g. `fedora/latest`) resolved correctly only for the + legacy `--remote` arg, failing with unknown image error for newer args. + - module_utils/basic.py - Fix `AnsibleModule.run_command()` to handle + `None` return from non-blocking pipe reads + (https://github.com/ansible/ansible/issues/86920). + * New upstream version 2.19.11 + - ansible-test - Replace FreeBSD 14.2 with 14.3. + - ansible-galaxy install - Ensure role requirements are passed as positional + arguments to `git clone`. Previously, a malicious role author could + inject arbitrary git configuration in role dependencies. (CVE-2026-11332) + (Closes: #1139175) + - module_utils sanitize_keys and remove_value functions now sort their input + to ensure matching subsets are always obscured. + + -- Lee Garrett Fri, 19 Jun 2026 18:56:20 +0200 + ansible-core (2.19.4-0+deb13u1) trixie; urgency=medium [ Lee Garrett ] diff -Nru ansible-core-2.19.4/debian/salsa-ci.yml ansible-core-2.19.11/debian/salsa-ci.yml --- ansible-core-2.19.4/debian/salsa-ci.yml 2025-08-26 22:54:39.000000000 +0000 +++ ansible-core-2.19.11/debian/salsa-ci.yml 2026-06-18 19:45:09.000000000 +0000 @@ -3,5 +3,5 @@ - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/recipes/debian.yml variables: - RELEASE: 'unstable' - SALSA_CI_IMAGES_LINTIAN: "${SALSA_CI_IMAGES}/lintian:unstable" + RELEASE: 'trixie' + SALSA_CI_IMAGES_LINTIAN: "${SALSA_CI_IMAGES}/lintian:trixie" diff -Nru ansible-core-2.19.4/lib/ansible/_internal/_yaml/_loader.py ansible-core-2.19.11/lib/ansible/_internal/_yaml/_loader.py --- ansible-core-2.19.4/lib/ansible/_internal/_yaml/_loader.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/lib/ansible/_internal/_yaml/_loader.py 2026-06-18 19:34:02.000000000 +0000 @@ -13,22 +13,20 @@ if HAS_LIBYAML: from yaml.cyaml import CParser - class _YamlParser(CParser): + class _Parser(CParser): def __init__(self, stream: str | bytes | _io.IOBase) -> None: if isinstance(stream, (str, bytes)): stream = AnsibleTagHelper.untag(stream) # PyYAML + libyaml barfs on str/bytes subclasses CParser.__init__(self, stream) - self.name = getattr(stream, 'name', None) # provide feature parity with the Python implementation (yaml.reader.Reader provides name) - else: from yaml.composer import Composer from yaml.reader import Reader from yaml.scanner import Scanner from yaml.parser import Parser - class _YamlParser(Reader, Scanner, Parser, Composer): # type: ignore[no-redef] + class _Parser(Reader, Scanner, Parser, Composer): # type: ignore[no-redef] def __init__(self, stream: str | bytes | _io.IOBase) -> None: Reader.__init__(self, stream) Scanner.__init__(self) @@ -36,6 +34,17 @@ Composer.__init__(self) +class _YamlParser(_Parser): + def __init__(self, stream: str | bytes | _io.IOBase) -> None: + super().__init__(stream) + + # The Python implementation of PyYAML (yaml.reader.Reader) provides self.name. + # However, it will fall back to "<...>" in various cases. + # The C implementation of PyYAML does not provide self.name. + # To provide consistency, name retrieval is re-implemented here. + self.name = getattr(stream, 'name', None) + + class AnsibleInstrumentedLoader(_YamlParser, AnsibleInstrumentedConstructor, Resolver): """Ansible YAML loader which supports Ansible custom behavior such as `Origin` tagging, but no Ansible-specific YAML tags.""" diff -Nru ansible-core-2.19.4/lib/ansible/cli/galaxy.py ansible-core-2.19.11/lib/ansible/cli/galaxy.py --- ansible-core-2.19.4/lib/ansible/cli/galaxy.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/lib/ansible/cli/galaxy.py 2026-06-18 19:34:02.000000000 +0000 @@ -80,8 +80,10 @@ if 'artifacts_manager' in kwargs: return wrapped_method(*args, **kwargs) - # FIXME: use validate_certs context from Galaxy servers when downloading collections - # .get used here for when this is used in a non-CLI context + # configures validate_certs for type 'url' only + # type 'galaxy' inherits and overrides resolved_validate_certs + # type 'git' recalculates resolved_validate_certs + # NOTE: .get used here for when this is used in a non-CLI context artifacts_manager_kwargs = {'validate_certs': context.CLIARGS.get('resolved_validate_certs', True)} keyring = context.CLIARGS.get('keyring', None) diff -Nru ansible-core-2.19.4/lib/ansible/config/base.yml ansible-core-2.19.11/lib/ansible/config/base.yml --- ansible-core-2.19.4/lib/ansible/config/base.yml 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/lib/ansible/config/base.yml 2026-06-18 19:34:02.000000000 +0000 @@ -291,7 +291,7 @@ Collections must be in nested *subdirectories*, not directly in these directories. For example, if ``COLLECTIONS_PATHS`` includes ``'{{ ANSIBLE_HOME ~ "/collections" }}'``, and you want to add ``my.collection`` to that directory, it must be saved as - ``'{{ ANSIBLE_HOME} ~ "/collections/ansible_collections/my/collection" }}'``. + ``'{{ ANSIBLE_HOME ~ "/collections/ansible_collections/my/collection" }}'``. default: '{{ ANSIBLE_HOME ~ "/collections:/usr/share/ansible/collections" }}' type: pathspec env: @@ -2272,3 +2272,14 @@ ini: - section: testing key: valid2 +_Z_TEST_ENTRY_3: + version_added: '2.21' + name: testentry + description: for tests + type: path + default: '{{ANSIBLE_HOME}}' + env: + - name: ANSIBLE_TEST_ENTRY3 + ini: + - section: testing + key: valid3 \ No newline at end of file diff -Nru ansible-core-2.19.4/lib/ansible/executor/module_common.py ansible-core-2.19.11/lib/ansible/executor/module_common.py --- ansible-core-2.19.4/lib/ansible/executor/module_common.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/lib/ansible/executor/module_common.py 2026-06-18 19:34:02.000000000 +0000 @@ -1357,6 +1357,9 @@ def _get_action_arg_defaults(action: str, task: Task, templar: TemplateEngine) -> dict[str, t.Any]: + """ + Get module_defaults that match or contain a fully qualified action/module name. + """ action_groups = task._parent._play._action_groups defaults = task.module_defaults @@ -1393,7 +1396,14 @@ def _apply_action_arg_defaults(action: str, task: Task, action_args: dict[str, t.Any], templar: Templar) -> dict[str, t.Any]: + """ + Finalize arguments from module_defaults and update with action_args. + + This is used by action plugins like gather_facts, package, and service, + which select modules to execute after normal task argument finalization. + """ args = _get_action_arg_defaults(action, task, templar._engine) + args = templar.template({k: v for k, v in args.items() if k not in action_args}) args.update(action_args) return args diff -Nru ansible-core-2.19.4/lib/ansible/galaxy/collection/concrete_artifact_manager.py ansible-core-2.19.11/lib/ansible/galaxy/collection/concrete_artifact_manager.py --- ansible-core-2.19.4/lib/ansible/galaxy/collection/concrete_artifact_manager.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/lib/ansible/galaxy/collection/concrete_artifact_manager.py 2026-06-18 19:34:02.000000000 +0000 @@ -28,7 +28,7 @@ from ansible import context from ansible.errors import AnsibleError from ansible.galaxy import get_collections_galaxy_meta_info -from ansible.galaxy.api import should_retry_error +from ansible.galaxy.api import should_retry_error, CollectionVersionMetadata, GalaxyAPI from ansible.galaxy.dependency_resolution.dataclasses import _GALAXY_YAML from ansible.galaxy.user_agent import user_agent from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text @@ -68,8 +68,7 @@ self._artifact_cache = {} # type: dict[bytes, bytes] self._galaxy_artifact_cache = {} # type: dict[Candidate | Requirement, bytes] self._artifact_meta_cache = {} # type: dict[bytes, dict[str, str | list[str] | dict[str, str] | None | t.Type[Sentinel]]] - self._galaxy_collection_cache = {} # type: dict[Candidate | Requirement, tuple[str, str, GalaxyToken]] - self._galaxy_collection_origin_cache = {} # type: dict[Candidate, tuple[str, list[dict[str, str]]]] + self._galaxy_collection_cache: dict[Candidate, tuple[CollectionVersionMetadata, GalaxyAPI]] = {} self._b_working_directory = b_working_directory # type: bytes self._supplemental_signature_cache = {} # type: dict[str, str] self._keyring = keyring # type: str @@ -104,15 +103,12 @@ def get_galaxy_artifact_source_info(self, collection): # type: (Candidate) -> dict[str, t.Union[str, list[dict[str, str]]]] - server = collection.src.api_server try: - download_url = self._galaxy_collection_cache[collection][0] - signatures_url, signatures = self._galaxy_collection_origin_cache[collection] + metadata, server = self._galaxy_collection_cache[collection] except KeyError as key_err: raise RuntimeError( - 'The is no known source for {coll!s}'. - format(coll=collection), + f"There is no known source for {collection!s}" ) from key_err return { @@ -120,14 +116,13 @@ "namespace": collection.namespace, "name": collection.name, "version": collection.ver, - "server": server, - "version_url": signatures_url, - "download_url": download_url, - "signatures": signatures, + "server": server.api_server, + "version_url": metadata.signatures_url, + "download_url": metadata.download_url, + "signatures": metadata.signatures, } - def get_galaxy_artifact_path(self, collection): - # type: (t.Union[Candidate, Requirement]) -> bytes + def get_galaxy_artifact_path(self, collection: Candidate) -> bytes: """Given a Galaxy-stored collection, return a cached path. If it's not yet on disk, this method downloads the artifact first. @@ -138,11 +133,10 @@ pass try: - url, sha256_hash, token = self._galaxy_collection_cache[collection] + metadata, api = self._galaxy_collection_cache[collection] except KeyError as key_err: raise RuntimeError( - 'There is no known source for {coll!s}'. - format(coll=collection), + f'There is no known source for {collection!s}' ) from key_err display.vvvv( @@ -152,39 +146,27 @@ try: b_artifact_path = _download_file( - url, + metadata.download_url, self._b_working_directory, - expected_hash=sha256_hash, - validate_certs=self._validate_certs, - token=token, + expected_hash=metadata.artifact_sha256, + validate_certs=api.validate_certs, + token=api.token, ) # type: bytes except URLError as err: raise AnsibleError( 'Failed to download collection tar ' - "from '{coll_src!s}': {download_err!s}". - format( - coll_src=to_native(collection.src), - download_err=to_native(err), - ), + f"from '{api!s}': {err!s}" ) from err except Exception as err: raise AnsibleError( 'Failed to download collection tar ' - "from '{coll_src!s}' due to the following unforeseen error: " - '{download_err!s}'. - format( - coll_src=to_native(collection.src), - download_err=to_native(err), - ), + f"from '{api!s}' due to the following unforeseen error: " + f'{err!s}' ) from err else: display.vvv( - "Collection '{coll!s}' obtained from " - 'server {server!s} {url!s}'.format( - coll=collection, server=collection.src or 'Galaxy', - url=collection.src.api_server if collection.src is not None - else '', - ) + f"Collection '{collection!s}' obtained from server {api!s} " + f"{api.api_server!s}" ) self._galaxy_artifact_cache[collection] = b_artifact_path @@ -338,15 +320,13 @@ self._artifact_meta_cache[collection.src] = collection_meta return collection_meta - def save_collection_source(self, collection, url, sha256_hash, token, signatures_url, signatures): - # type: (Candidate, str, str, GalaxyToken, str, list[dict[str, str]]) -> None - """Store collection URL, SHA256 hash and Galaxy API token. + def save_collection_source(self, collection: Candidate, metadata: CollectionVersionMetadata, api: GalaxyAPI) -> None: + """Store collection version metadata and origin. This is a hook that is supposed to be called before attempting to download Galaxy-based collections with ``get_galaxy_artifact_path()``. """ - self._galaxy_collection_cache[collection] = url, sha256_hash, token - self._galaxy_collection_origin_cache[collection] = signatures_url, signatures + self._galaxy_collection_cache[collection] = (metadata, api) @classmethod @contextmanager diff -Nru ansible-core-2.19.4/lib/ansible/galaxy/collection/galaxy_api_proxy.py ansible-core-2.19.11/lib/ansible/galaxy/collection/galaxy_api_proxy.py --- ansible-core-2.19.4/lib/ansible/galaxy/collection/galaxy_api_proxy.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/lib/ansible/galaxy/collection/galaxy_api_proxy.py 2026-06-18 19:34:02.000000000 +0000 @@ -146,11 +146,8 @@ else: self._concrete_art_mgr.save_collection_source( collection_candidate, - version_metadata.download_url, - version_metadata.artifact_sha256, - api.token, - version_metadata.signatures_url, - version_metadata.signatures, + version_metadata, + api ) return version_metadata diff -Nru ansible-core-2.19.4/lib/ansible/module_utils/ansible_release.py ansible-core-2.19.11/lib/ansible/module_utils/ansible_release.py --- ansible-core-2.19.4/lib/ansible/module_utils/ansible_release.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/lib/ansible/module_utils/ansible_release.py 2026-06-18 19:34:02.000000000 +0000 @@ -17,6 +17,6 @@ from __future__ import annotations -__version__ = '2.19.4' +__version__ = '2.19.11' __author__ = 'Ansible, Inc.' __codename__ = "What Is and What Should Never Be" diff -Nru ansible-core-2.19.4/lib/ansible/module_utils/basic.py ansible-core-2.19.11/lib/ansible/module_utils/basic.py --- ansible-core-2.19.4/lib/ansible/module_utils/basic.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/lib/ansible/module_utils/basic.py 2026-06-18 19:34:02.000000000 +0000 @@ -2090,7 +2090,13 @@ stdout_changed = False for key, event in events: b_chunk = key.fileobj.read(32768) - if not b_chunk and b_chunk is not None: + if b_chunk is None: + # Non-blocking read returned None (no data currently available). + # This can happen with certain file-like objects or in edge cases. + # Skip this chunk and try again on next select iteration. + continue + if not b_chunk: + # Empty bytes received, EOF reached selector.unregister(key.fileobj) elif key.fileobj == cmd.stdout: stdout += b_chunk @@ -2150,14 +2156,16 @@ with open(filename, 'a') as fh: fh.write(str) - def bytes_to_human(self, size): + @staticmethod + def bytes_to_human(size: int) -> str: return bytes_to_human(size) # for backwards compatibility pretty_bytes = bytes_to_human - def human_to_bytes(self, number, isbits=False): - return human_to_bytes(number, isbits) + @staticmethod + def human_to_bytes(number: str, isbits: bool = False) -> int: + return human_to_bytes(number, isbits=isbits) # # Backwards compat diff -Nru ansible-core-2.19.4/lib/ansible/module_utils/common/parameters.py ansible-core-2.19.11/lib/ansible/module_utils/common/parameters.py --- ansible-core-2.19.4/lib/ansible/module_utils/common/parameters.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/lib/ansible/module_utils/common/parameters.py 2026-06-18 19:34:02.000000000 +0000 @@ -512,7 +512,7 @@ return no_log_values -def _sanitize_keys_conditions(value, no_log_strings, ignore_keys, deferred_removals): +def _sanitize_keys_conditions(value, deferred_removals): """ Helper method to :func:`sanitize_keys` to build ``deferred_removals`` and avoid deep recursion. """ if isinstance(value, (text_type, binary_type)): return value @@ -880,8 +880,9 @@ deferred_removals = deque() - no_log_strings = [to_native(s, errors='surrogate_or_strict') for s in no_log_strings] - new_value = _sanitize_keys_conditions(obj, no_log_strings, ignore_keys, deferred_removals) + # sort ensuring we always handle longer strings vs subsets + no_log_strings = sorted([to_native(s, errors='surrogate_or_strict') for s in no_log_strings], key=len, reverse=True) + new_value = _sanitize_keys_conditions(obj, deferred_removals) while deferred_removals: old_data, new_data = deferred_removals.popleft() @@ -889,15 +890,15 @@ if isinstance(new_data, Mapping): for old_key, old_elem in old_data.items(): if old_key in ignore_keys or old_key.startswith('_ansible'): - new_data[old_key] = _sanitize_keys_conditions(old_elem, no_log_strings, ignore_keys, deferred_removals) + new_data[old_key] = _sanitize_keys_conditions(old_elem, deferred_removals) else: # Sanitize the old key. We take advantage of the sanitizing code in # _remove_values_conditions() rather than recreating it here. new_key = _remove_values_conditions(old_key, no_log_strings, None) - new_data[new_key] = _sanitize_keys_conditions(old_elem, no_log_strings, ignore_keys, deferred_removals) + new_data[new_key] = _sanitize_keys_conditions(old_elem, deferred_removals) else: for elem in old_data: - new_elem = _sanitize_keys_conditions(elem, no_log_strings, ignore_keys, deferred_removals) + new_elem = _sanitize_keys_conditions(elem, deferred_removals) if isinstance(new_data, MutableSequence): new_data.append(new_elem) elif isinstance(new_data, MutableSet): @@ -920,7 +921,8 @@ deferred_removals = deque() - no_log_strings = [to_native(s, errors='surrogate_or_strict') for s in no_log_strings] + # sort ensuring we always handle longer strings vs subsets + no_log_strings = sorted([to_native(s, errors='surrogate_or_strict') for s in no_log_strings], key=len, reverse=True) new_value = _remove_values_conditions(value, no_log_strings, deferred_removals) while deferred_removals: diff -Nru ansible-core-2.19.4/lib/ansible/module_utils/common/text/formatters.py ansible-core-2.19.11/lib/ansible/module_utils/common/text/formatters.py --- ansible-core-2.19.4/lib/ansible/module_utils/common/text/formatters.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/lib/ansible/module_utils/common/text/formatters.py 2026-06-18 19:34:02.000000000 +0000 @@ -60,7 +60,7 @@ if 'Mb'/'Kb'/... is passed, the ValueError will be rased. When isbits is True, converts bits from a human-readable format to integer. - example: human_to_bytes('1Mb', isbits=True) returns 8388608 (int) - + example: human_to_bytes('1Mb', isbits=True) returns 1048576 (int) - string bits representation was passed and return as a number or bits. The function expects 'b' (lowercase) as a bit identifier, e.g. 'Mb'/'Kb'/etc. if 'MB'/'KB'/... is passed, the ValueError will be rased. diff -Nru ansible-core-2.19.4/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1 ansible-core-2.19.11/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1 --- ansible-core-2.19.4/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1 2026-06-18 19:34:02.000000000 +0000 @@ -378,7 +378,7 @@ $originalEnv = @{} try { 'LIB' | ForEach-Object -Process { - $value = Get-Item -LiteralPath "Env:\$_" -ErrorAction SilentlyContinue + $value = (Get-Item -LiteralPath "Env:\$_" -ErrorAction SilentlyContinue).Value if ($value) { $originalEnv[$_] = $value Remove-Item -LiteralPath "Env:\$_" diff -Nru ansible-core-2.19.4/lib/ansible/modules/get_url.py ansible-core-2.19.11/lib/ansible/modules/get_url.py --- ansible-core-2.19.4/lib/ansible/modules/get_url.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/lib/ansible/modules/get_url.py 2026-06-18 19:34:02.000000000 +0000 @@ -477,17 +477,13 @@ return urlsplit(checksum).scheme in supported_schemes -def parse_digest_lines(filename, lines): +def parse_digest_lines(filename: str, lines: list[str]) -> list[tuple[str, str]]: """Returns a list of tuple containing the filename and digest depending upon the lines provided - - Args: - filename (str): Name of the filename, used only when the digest is one-liner - lines (list): A list of lines containing filenames and checksums """ checksum_map = [] BSD_DIGEST_LINE = re.compile(r'^(\w+) ?\((?P.+)\) ?= (?P[\w.]+)$') - GNU_DIGEST_LINE = re.compile(r'^(?P[\w.]+) ([ *])(?P.+)$') + GNU_DIGEST_LINE = re.compile(r'^(?P[\w.]+)\s+(\*|\.\/|\.)?(?P.+)$') if len(lines) == 1 and len(lines[0].split()) == 1: # Only a single line with a single string diff -Nru ansible-core-2.19.4/lib/ansible/plugins/action/__init__.py ansible-core-2.19.11/lib/ansible/plugins/action/__init__.py --- ansible-core-2.19.4/lib/ansible/plugins/action/__init__.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/lib/ansible/plugins/action/__init__.py 2026-06-18 19:34:02.000000000 +0000 @@ -456,7 +456,7 @@ become_user = self.get_become_option('become_user') return bool(become_user and become_user not in admin_users + [remote_user]) - def _make_tmp_path(self, remote_user=None): + def _make_tmp_path(self, remote_user: str | None = None) -> str: """ Create and return a temporary path on a remote box. """ @@ -494,7 +494,7 @@ output = ('Failed to create temporary directory. ' 'In some cases, you may have been able to authenticate and did not have permissions on the target directory. ' 'Consider changing the remote tmp path in ansible.cfg to a path rooted in "/tmp", for more error information use -vvv. ' - 'Failed command was: %s, exited with result %d' % (cmd, result['rc'])) + 'Failed command was: %s, exited with result %d' % (cmd.command, result['rc'])) if 'stdout' in result and result['stdout'] != u'': output = output + u", stdout output: %s" % result['stdout'] if display.verbosity > 3 and 'stderr' in result and result['stderr'] != u'': diff -Nru ansible-core-2.19.4/lib/ansible/plugins/become/__init__.py ansible-core-2.19.11/lib/ansible/plugins/become/__init__.py --- ansible-core-2.19.4/lib/ansible/plugins/become/__init__.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/lib/ansible/plugins/become/__init__.py 2026-06-18 19:34:02.000000000 +0000 @@ -105,7 +105,7 @@ b_success = to_bytes(self.success) return any(b_success in l.rstrip() for l in b_output.splitlines(True)) - def check_password_prompt(self, b_output): + def check_password_prompt(self, b_output: bytes) -> bool: """ checks if the expected password prompt exists in b_output """ if self.prompt: b_prompt = to_bytes(self.prompt).strip() diff -Nru ansible-core-2.19.4/lib/ansible/plugins/connection/_paramiko_ssh.py ansible-core-2.19.11/lib/ansible/plugins/connection/_paramiko_ssh.py --- ansible-core-2.19.4/lib/ansible/plugins/connection/_paramiko_ssh.py 1970-01-01 00:00:00.000000000 +0000 +++ ansible-core-2.19.11/lib/ansible/plugins/connection/_paramiko_ssh.py 2026-06-18 19:34:02.000000000 +0000 @@ -0,0 +1,713 @@ +# (c) 2012, Michael DeHaan +# (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import annotations + +DOCUMENTATION = """ + author: Ansible Core Team + name: paramiko + short_description: Run tasks via Python SSH (paramiko) + deprecated: + removed_in: "2.21" + why: Paramiko versions older than 3.3.2 and 3.4.1 will be incompatible with cryptography 48.0.0. + alternatives: ansible.builtin.ssh + description: + - Use the Python SSH implementation (Paramiko) to connect to targets + - The paramiko transport is provided because many distributions, in particular EL6 and before do not support ControlPersist + in their SSH implementations. + - This is needed on the Ansible control machine to be reasonably efficient with connections. + Thus paramiko is faster for most users on these platforms. + Users with ControlPersist capability can consider using -c ssh or configuring the transport in the configuration file. + - This plugin also borrows a lot of settings from the ssh plugin as they both cover the same protocol. + version_added: "0.1" + options: + remote_addr: + description: + - Address of the remote target + default: inventory_hostname + type: string + vars: + - name: inventory_hostname + - name: ansible_host + - name: ansible_ssh_host + - name: ansible_paramiko_host + port: + description: Remote port to connect to. + type: int + default: 22 + ini: + - section: defaults + key: remote_port + - section: paramiko_connection + key: remote_port + version_added: '2.15' + env: + - name: ANSIBLE_REMOTE_PORT + - name: ANSIBLE_REMOTE_PARAMIKO_PORT + version_added: '2.15' + vars: + - name: ansible_port + - name: ansible_ssh_port + - name: ansible_paramiko_port + version_added: '2.15' + keyword: + - name: port + remote_user: + description: + - User to login/authenticate as + - Can be set from the CLI via the C(--user) or C(-u) options. + type: string + vars: + - name: ansible_user + - name: ansible_ssh_user + - name: ansible_paramiko_user + env: + - name: ANSIBLE_REMOTE_USER + - name: ANSIBLE_PARAMIKO_REMOTE_USER + version_added: '2.5' + ini: + - section: defaults + key: remote_user + - section: paramiko_connection + key: remote_user + version_added: '2.5' + keyword: + - name: remote_user + password: + description: + - Secret used to either login the ssh server or as a passphrase for ssh keys that require it + - Can be set from the CLI via the C(--ask-pass) option. + type: string + vars: + - name: ansible_password + - name: ansible_ssh_pass + - name: ansible_ssh_password + - name: ansible_paramiko_pass + - name: ansible_paramiko_password + version_added: '2.5' + use_rsa_sha2_algorithms: + description: + - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys + - On paramiko versions older than 2.9, this only affects hostkeys + - For behavior matching paramiko<2.9 set this to V(False) + vars: + - name: ansible_paramiko_use_rsa_sha2_algorithms + ini: + - {key: use_rsa_sha2_algorithms, section: paramiko_connection} + env: + - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS} + default: True + type: boolean + version_added: '2.14' + host_key_auto_add: + description: 'Automatically add host keys' + env: [{name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD}] + ini: + - {key: host_key_auto_add, section: paramiko_connection} + type: boolean + look_for_keys: + default: True + description: 'False to disable searching for private key files in ~/.ssh/' + env: [{name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS}] + ini: + - {key: look_for_keys, section: paramiko_connection} + type: boolean + proxy_command: + default: '' + description: + - Proxy information for running the connection via a jumphost. + type: string + env: [{name: ANSIBLE_PARAMIKO_PROXY_COMMAND}] + ini: + - {key: proxy_command, section: paramiko_connection} + vars: + - name: ansible_paramiko_proxy_command + version_added: '2.15' + pty: + default: True + description: 'SUDO usually requires a PTY, True to give a PTY and False to not give a PTY.' + env: + - name: ANSIBLE_PARAMIKO_PTY + ini: + - section: paramiko_connection + key: pty + type: boolean + record_host_keys: + default: True + description: 'Save the host keys to a file' + env: [{name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS}] + ini: + - section: paramiko_connection + key: record_host_keys + type: boolean + host_key_checking: + description: 'Set this to "False" if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host' + type: boolean + default: True + env: + - name: ANSIBLE_HOST_KEY_CHECKING + - name: ANSIBLE_SSH_HOST_KEY_CHECKING + version_added: '2.5' + - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING + version_added: '2.5' + ini: + - section: defaults + key: host_key_checking + - section: paramiko_connection + key: host_key_checking + version_added: '2.5' + vars: + - name: ansible_host_key_checking + version_added: '2.5' + - name: ansible_ssh_host_key_checking + version_added: '2.5' + - name: ansible_paramiko_host_key_checking + version_added: '2.5' + use_persistent_connections: + description: 'Toggles the use of persistence for connections' + type: boolean + default: False + env: + - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS + ini: + - section: defaults + key: use_persistent_connections + banner_timeout: + type: float + default: 30 + version_added: '2.14' + description: + - Configures, in seconds, the amount of time to wait for the SSH + banner to be presented. This option is supported by paramiko + version 1.15.0 or newer. + ini: + - section: paramiko_connection + key: banner_timeout + env: + - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT + timeout: + type: int + default: 10 + description: Number of seconds until the plugin gives up on failing to establish a TCP connection. + ini: + - section: defaults + key: timeout + - section: ssh_connection + key: timeout + version_added: '2.11' + - section: paramiko_connection + key: timeout + version_added: '2.15' + env: + - name: ANSIBLE_TIMEOUT + - name: ANSIBLE_SSH_TIMEOUT + version_added: '2.11' + - name: ANSIBLE_PARAMIKO_TIMEOUT + version_added: '2.15' + vars: + - name: ansible_ssh_timeout + version_added: '2.11' + - name: ansible_paramiko_timeout + version_added: '2.15' + cli: + - name: timeout + private_key_file: + description: + - Path to private key file to use for authentication. + type: string + ini: + - section: defaults + key: private_key_file + - section: paramiko_connection + key: private_key_file + version_added: '2.15' + env: + - name: ANSIBLE_PRIVATE_KEY_FILE + - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE + version_added: '2.15' + vars: + - name: ansible_private_key_file + - name: ansible_ssh_private_key_file + - name: ansible_paramiko_private_key_file + version_added: '2.15' + cli: + - name: private_key_file + option: '--private-key' +""" + +import os +import socket +import tempfile +import traceback +import fcntl +import re +import typing as t + +from ansible.module_utils.compat.version import LooseVersion +from binascii import hexlify + +from ansible.errors import ( + AnsibleAuthenticationFailure, + AnsibleConnectionFailure, + AnsibleError, + AnsibleFileNotFound, +) + +from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text +from ansible.module_utils.compat.paramiko import _PARAMIKO_IMPORT_ERR as PARAMIKO_IMPORT_ERR, _paramiko as paramiko +from ansible.plugins.connection import ConnectionBase +from ansible.utils.display import Display +from ansible.utils.path import makedirs_safe +from ansible.module_utils._internal import _deprecator + +display = Display() + + +AUTHENTICITY_MSG = """ +paramiko: The authenticity of host '%s' can't be established. +The %s key fingerprint is %s. +Are you sure you want to continue connecting (yes/no)? +""" + +# SSH Options Regex +SETTINGS_REGEX = re.compile(r'(\w+)(?:\s*=\s*|\s+)(.+)') + +MissingHostKeyPolicy: type = object +if paramiko: + MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy + + +class MyAddPolicy(MissingHostKeyPolicy): + """ + Based on AutoAddPolicy in paramiko so we can determine when keys are added + + and also prompt for input. + + Policy for automatically adding the hostname and new host key to the + local L{HostKeys} object, and saving it. This is used by L{SSHClient}. + """ + + def __init__(self, connection: Connection) -> None: + self.connection = connection + self._options = connection._options + + def missing_host_key(self, client, hostname, key) -> None: + + if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))): + + fingerprint = hexlify(key.get_fingerprint()) + ktype = key.get_name() + + if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence: + # don't print the prompt string since the user cannot respond + # to the question anyway + raise AnsibleError(AUTHENTICITY_MSG[1:92] % (hostname, ktype, fingerprint)) + + inp = to_text( + display.prompt_until(AUTHENTICITY_MSG % (hostname, ktype, fingerprint), private=False), + errors='surrogate_or_strict' + ) + + if inp not in ['yes', 'y', '']: + raise AnsibleError("host connection rejected by user") + + key._added_by_ansible_this_time = True + + # existing implementation below: + client._host_keys.add(hostname, key.get_name(), key) + + # host keys are actually saved in close() function below + # in order to control ordering. + + +# keep connection objects on a per host basis to avoid repeated attempts to reconnect + +SSH_CONNECTION_CACHE: dict[str, paramiko.client.SSHClient] = {} +SFTP_CONNECTION_CACHE: dict[str, paramiko.sftp_client.SFTPClient] = {} + + +class Connection(ConnectionBase): + """ SSH based connections with Paramiko """ + + transport = 'paramiko' + _log_channel: str | None = None + + def __init__(self, *args, **kwargs): + display.deprecated( # pylint: disable=ansible-deprecated-unnecessary-collection-name + msg='The paramiko connection plugin is deprecated.', + version='2.21', + deprecator=_deprecator.ANSIBLE_CORE_DEPRECATOR, # entire plugin being removed; this improves the messaging + ) + + super().__init__(*args, **kwargs) + + def _cache_key(self) -> str: + return "%s__%s__" % (self.get_option('remote_addr'), self.get_option('remote_user')) + + def _connect(self) -> Connection: + cache_key = self._cache_key() + if cache_key in SSH_CONNECTION_CACHE: + self.ssh = SSH_CONNECTION_CACHE[cache_key] + else: + self.ssh = SSH_CONNECTION_CACHE[cache_key] = self._connect_uncached() + + self._connected = True + return self + + def _set_log_channel(self, name: str) -> None: + """Mimic paramiko.SSHClient.set_log_channel""" + self._log_channel = name + + def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]: + proxy_command = self.get_option('proxy_command') or None + + sock_kwarg = {} + if proxy_command: + replacers = { + '%h': self.get_option('remote_addr'), + '%p': port, + '%r': self.get_option('remote_user') + } + for find, replace in replacers.items(): + proxy_command = proxy_command.replace(find, str(replace)) + try: + sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)} + display.vvv("CONFIGURE PROXY COMMAND FOR CONNECTION: %s" % proxy_command, host=self.get_option('remote_addr')) + except AttributeError: + display.warning('Paramiko ProxyCommand support unavailable. ' + 'Please upgrade to Paramiko 1.9.0 or newer. ' + 'Not using configured ProxyCommand') + + return sock_kwarg + + def _connect_uncached(self) -> paramiko.SSHClient: + """ activates the connection object """ + + if paramiko is None: + raise AnsibleError("paramiko is not installed: %s" % to_native(PARAMIKO_IMPORT_ERR)) + + port = self.get_option('port') + display.vvv("ESTABLISH PARAMIKO SSH CONNECTION FOR USER: %s on PORT %s TO %s" % (self.get_option('remote_user'), port, self.get_option('remote_addr')), + host=self.get_option('remote_addr')) + + ssh = paramiko.SSHClient() + + # Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently + # is keeping or omitting rsa-sha2 algorithms + # default_keys: t.Tuple[str] = () + paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ()) + paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ()) + use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms') + disabled_algorithms: t.Dict[str, t.Iterable[str]] = {} + if not use_rsa_sha2_algorithms: + if paramiko_preferred_pubkeys: + disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a) + if paramiko_preferred_hostkeys: + disabled_algorithms['keys'] = tuple(a for a in paramiko_preferred_hostkeys if 'rsa-sha2' in a) + + # override paramiko's default logger name + if self._log_channel is not None: + ssh.set_log_channel(self._log_channel) + + self.keyfile = os.path.expanduser("~/.ssh/known_hosts") + + if self.get_option('host_key_checking'): + for ssh_known_hosts in ("/etc/ssh/ssh_known_hosts", "/etc/openssh/ssh_known_hosts"): + try: + # TODO: check if we need to look at several possible locations, possible for loop + ssh.load_system_host_keys(ssh_known_hosts) + break + except OSError: + pass # file was not found, but not required to function + ssh.load_system_host_keys() + + ssh_connect_kwargs = self._parse_proxy_command(port) + + ssh.set_missing_host_key_policy(MyAddPolicy(self)) + + conn_password = self.get_option('password') + + allow_agent = True + + if conn_password is not None: + allow_agent = False + + try: + key_filename = None + if self.get_option('private_key_file'): + key_filename = os.path.expanduser(self.get_option('private_key_file')) + + # paramiko 2.2 introduced auth_timeout parameter + if LooseVersion(paramiko.__version__) >= LooseVersion('2.2.0'): + ssh_connect_kwargs['auth_timeout'] = self.get_option('timeout') + + # paramiko 1.15 introduced banner timeout parameter + if LooseVersion(paramiko.__version__) >= LooseVersion('1.15.0'): + ssh_connect_kwargs['banner_timeout'] = self.get_option('banner_timeout') + + ssh.connect( + self.get_option('remote_addr').lower(), + username=self.get_option('remote_user'), + allow_agent=allow_agent, + look_for_keys=self.get_option('look_for_keys'), + key_filename=key_filename, + password=conn_password, + timeout=self.get_option('timeout'), + port=port, + disabled_algorithms=disabled_algorithms, + **ssh_connect_kwargs, + ) + except paramiko.ssh_exception.BadHostKeyException as e: + raise AnsibleConnectionFailure('host key mismatch for %s' % e.hostname) + except paramiko.ssh_exception.AuthenticationException as ex: + raise AnsibleAuthenticationFailure() from ex + except Exception as ex: + msg = str(ex) + if u"PID check failed" in msg: + raise AnsibleError("paramiko version issue, please upgrade paramiko on the machine running ansible") from ex + elif u"Private key file is encrypted" in msg: + msg = 'ssh %s@%s:%s : %s\nTo connect as a different user, use -u .' % ( + self.get_option('remote_user'), self.get_options('remote_addr'), port, msg) + raise AnsibleConnectionFailure(msg) from ex + else: + raise AnsibleConnectionFailure(msg) from ex + + return ssh + + def exec_command(self, cmd: str, in_data: bytes | None = None, sudoable: bool = True) -> tuple[int, bytes, bytes]: + """ run a command on the remote host """ + + super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable) + + if in_data: + raise AnsibleError("Internal Error: this module does not support optimized module pipelining") + + bufsize = 4096 + + try: + self.ssh.get_transport().set_keepalive(5) + chan = self.ssh.get_transport().open_session() + except Exception as e: + text_e = to_text(e) + msg = u"Failed to open session" + if text_e: + msg += u": %s" % text_e + raise AnsibleConnectionFailure(to_native(msg)) + + # sudo usually requires a PTY (cf. requiretty option), therefore + # we give it one by default (pty=True in ansible.cfg), and we try + # to initialise from the calling environment when sudoable is enabled + if self.get_option('pty') and sudoable: + chan.get_pty(term=os.getenv('TERM', 'vt100'), width=int(os.getenv('COLUMNS', 0)), height=int(os.getenv('LINES', 0))) + + display.vvv("EXEC %s" % cmd, host=self.get_option('remote_addr')) + + cmd = to_bytes(cmd, errors='surrogate_or_strict') + + no_prompt_out = b'' + no_prompt_err = b'' + become_output = b'' + + try: + chan.exec_command(cmd) + if self.become and self.become.expect_prompt(): + passprompt = False + become_sucess = False + while not (become_sucess or passprompt): + display.debug('Waiting for Privilege Escalation input') + + chunk = chan.recv(bufsize) + display.debug("chunk is: %r" % chunk) + if not chunk: + if b'unknown user' in become_output: + n_become_user = to_native(self.become.get_option('become_user')) + raise AnsibleError('user %s does not exist' % n_become_user) + else: + break + # raise AnsibleError('ssh connection closed waiting for password prompt') + become_output += chunk + + # need to check every line because we might get lectured + # and we might get the middle of a line in a chunk + for line in become_output.splitlines(True): + if self.become.check_success(line): + become_sucess = True + break + elif self.become.check_password_prompt(line): + passprompt = True + break + + if passprompt: + if self.become: + become_pass = self.become.get_option('become_pass') + chan.sendall(to_bytes(become_pass, errors='surrogate_or_strict') + b'\n') + else: + raise AnsibleError("A password is required but none was supplied") + else: + no_prompt_out += become_output + no_prompt_err += become_output + except socket.timeout: + raise AnsibleError('ssh timed out waiting for privilege escalation.\n' + to_text(become_output)) + + stdout = b''.join(chan.makefile('rb', bufsize)) + stderr = b''.join(chan.makefile_stderr('rb', bufsize)) + + return (chan.recv_exit_status(), no_prompt_out + stdout, no_prompt_out + stderr) + + def put_file(self, in_path: str, out_path: str) -> None: + """ transfer a file from local to remote """ + + super(Connection, self).put_file(in_path, out_path) + + display.vvv("PUT %s TO %s" % (in_path, out_path), host=self.get_option('remote_addr')) + + if not os.path.exists(to_bytes(in_path, errors='surrogate_or_strict')): + raise AnsibleFileNotFound("file or module does not exist: %s" % in_path) + + try: + self.sftp = self.ssh.open_sftp() + except Exception as e: + raise AnsibleError("failed to open a SFTP connection (%s)" % e) + + try: + self.sftp.put(to_bytes(in_path, errors='surrogate_or_strict'), to_bytes(out_path, errors='surrogate_or_strict')) + except OSError as ex: + raise AnsibleError(f"Failed to transfer file to {out_path!r}.") from ex + + def _connect_sftp(self) -> paramiko.sftp_client.SFTPClient: + + cache_key = "%s__%s__" % (self.get_option('remote_addr'), self.get_option('remote_user')) + if cache_key in SFTP_CONNECTION_CACHE: + return SFTP_CONNECTION_CACHE[cache_key] + else: + result = SFTP_CONNECTION_CACHE[cache_key] = self._connect().ssh.open_sftp() + return result + + def fetch_file(self, in_path: str, out_path: str) -> None: + """ save a remote file to the specified path """ + + super(Connection, self).fetch_file(in_path, out_path) + + display.vvv("FETCH %s TO %s" % (in_path, out_path), host=self.get_option('remote_addr')) + + try: + self.sftp = self._connect_sftp() + except Exception as e: + raise AnsibleError("failed to open a SFTP connection (%s)" % to_native(e)) + + try: + self.sftp.get(to_bytes(in_path, errors='surrogate_or_strict'), to_bytes(out_path, errors='surrogate_or_strict')) + except OSError as ex: + raise AnsibleError(f"Failed to transfer file from {in_path!r}.") from ex + + def _any_keys_added(self) -> bool: + + for hostname, keys in self.ssh._host_keys.items(): + for keytype, key in keys.items(): + added_this_time = getattr(key, '_added_by_ansible_this_time', False) + if added_this_time: + return True + return False + + def _save_ssh_host_keys(self, filename: str) -> None: + """ + not using the paramiko save_ssh_host_keys function as we want to add new SSH keys at the bottom so folks + don't complain about it :) + """ + + if not self._any_keys_added(): + return + + path = os.path.expanduser("~/.ssh") + makedirs_safe(path) + + with open(filename, 'w') as f: + + for hostname, keys in self.ssh._host_keys.items(): + + for keytype, key in keys.items(): + + # was f.write + added_this_time = getattr(key, '_added_by_ansible_this_time', False) + if not added_this_time: + f.write("%s %s %s\n" % (hostname, keytype, key.get_base64())) + + for hostname, keys in self.ssh._host_keys.items(): + + for keytype, key in keys.items(): + added_this_time = getattr(key, '_added_by_ansible_this_time', False) + if added_this_time: + f.write("%s %s %s\n" % (hostname, keytype, key.get_base64())) + + def reset(self) -> None: + if not self._connected: + return + self.close() + self._connect() + + def close(self) -> None: + """ terminate the connection """ + + cache_key = self._cache_key() + SSH_CONNECTION_CACHE.pop(cache_key, None) + SFTP_CONNECTION_CACHE.pop(cache_key, None) + + if hasattr(self, 'sftp'): + if self.sftp is not None: + self.sftp.close() + + if self.get_option('host_key_checking') and self.get_option('record_host_keys') and self._any_keys_added(): + + # add any new SSH host keys -- warning -- this could be slow + # (This doesn't acquire the connection lock because it needs + # to exclude only other known_hosts writers, not connections + # that are starting up.) + lockfile = self.keyfile.replace("known_hosts", ".known_hosts.lock") + dirname = os.path.dirname(self.keyfile) + makedirs_safe(dirname) + + KEY_LOCK = open(lockfile, 'w') + fcntl.lockf(KEY_LOCK, fcntl.LOCK_EX) + + try: + # just in case any were added recently + + self.ssh.load_system_host_keys() + self.ssh._host_keys.update(self.ssh._system_host_keys) + + # gather information about the current key file, so + # we can ensure the new file has the correct mode/owner + + key_dir = os.path.dirname(self.keyfile) + if os.path.exists(self.keyfile): + key_stat = os.stat(self.keyfile) + mode = key_stat.st_mode + uid = key_stat.st_uid + gid = key_stat.st_gid + else: + mode = 33188 + uid = os.getuid() + gid = os.getgid() + + # Save the new keys to a temporary file and move it into place + # rather than rewriting the file. We set delete=False because + # the file will be moved into place rather than cleaned up. + + tmp_keyfile = tempfile.NamedTemporaryFile(dir=key_dir, delete=False) + os.chmod(tmp_keyfile.name, mode & 0o7777) + os.chown(tmp_keyfile.name, uid, gid) + + self._save_ssh_host_keys(tmp_keyfile.name) + tmp_keyfile.close() + + os.rename(tmp_keyfile.name, self.keyfile) + + except Exception: + + # unable to save keys, including scenario when key was invalid + # and caught earlier + traceback.print_exc() + fcntl.lockf(KEY_LOCK, fcntl.LOCK_UN) + + self.ssh.close() + self._connected = False diff -Nru ansible-core-2.19.4/lib/ansible/plugins/connection/local.py ansible-core-2.19.11/lib/ansible/plugins/connection/local.py --- ansible-core-2.19.4/lib/ansible/plugins/connection/local.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/lib/ansible/plugins/connection/local.py 2026-06-18 19:34:02.000000000 +0000 @@ -201,8 +201,8 @@ raise AnsibleError(become_error_msg('Premature end of stream')) if expect_password_prompt and ( - self.become.check_password_prompt(become_stdout[last_stdout_prompt_offset:]) or - self.become.check_password_prompt(become_stderr[last_stderr_prompt_offset:]) + self.become.check_password_prompt(bytes(become_stdout[last_stdout_prompt_offset:])) or + self.become.check_password_prompt(bytes(become_stderr[last_stderr_prompt_offset:])) ): if sent_password: raise AnsibleError(become_error_msg('Duplicate become password prompt encountered')) diff -Nru ansible-core-2.19.4/lib/ansible/plugins/connection/paramiko_ssh.py ansible-core-2.19.11/lib/ansible/plugins/connection/paramiko_ssh.py --- ansible-core-2.19.4/lib/ansible/plugins/connection/paramiko_ssh.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/lib/ansible/plugins/connection/paramiko_ssh.py 2026-06-18 19:34:02.000000000 +0000 @@ -7,6 +7,10 @@ author: Ansible Core Team name: paramiko short_description: Run tasks via Python SSH (paramiko) + deprecated: + removed_in: "2.21" + why: Paramiko versions older than 3.3.2 and 3.4.1 will be incompatible with cryptography 48.0.0. + alternatives: ansible.builtin.ssh description: - Use the Python SSH implementation (Paramiko) to connect to targets - The paramiko transport is provided because many distributions, in particular EL6 and before do not support ControlPersist diff -Nru ansible-core-2.19.4/lib/ansible/plugins/connection/psrp.py ansible-core-2.19.11/lib/ansible/plugins/connection/psrp.py --- ansible-core-2.19.4/lib/ansible/plugins/connection/psrp.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/lib/ansible/plugins/connection/psrp.py 2026-06-18 19:34:02.000000000 +0000 @@ -769,9 +769,14 @@ stderr_list += self.host.ui.stderr stderr = "".join([to_text(o) for o in stderr_list]) + log_stdout = stdout + log_stderr = stderr + if self._play_context.no_log: + log_stdout = log_stderr = '' + display.vvvvv("PSRP RC: %d" % rc, host=self._psrp_host) - display.vvvvv("PSRP STDOUT: %s" % stdout, host=self._psrp_host) - display.vvvvv("PSRP STDERR: %s" % stderr, host=self._psrp_host) + display.vvvvv(f"PSRP STDOUT: {log_stdout}", host=self._psrp_host) + display.vvvvv(f"PSRP STDERR: {log_stderr}", host=self._psrp_host) # reset the host back output back to defaults, needed if running # multiple pipelines on the same RunspacePool diff -Nru ansible-core-2.19.4/lib/ansible/plugins/connection/winrm.py ansible-core-2.19.11/lib/ansible/plugins/connection/winrm.py --- ansible-core-2.19.4/lib/ansible/plugins/connection/winrm.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/lib/ansible/plugins/connection/winrm.py 2026-06-18 19:34:02.000000000 +0000 @@ -31,6 +31,8 @@ remote_user: description: - The user to log in as to the Windows machine + - If O(transport) is not defined, the authentication used will be V(kerberos) if the user is in the UPN format V(user@domain), + otherwise it will be V(basic) vars: - name: ansible_user - name: ansible_winrm_user @@ -75,7 +77,8 @@ transport: description: - List of winrm transports to attempt to use (ssl, plaintext, kerberos, etc) - - If None (the default) the plugin will try to automatically guess the correct list + - If None (the default) the plugin will try to automatically guess the correct list. It will use + V(kerberos) if the username looks like a UPN V(user@domain), otherwise it will use V(basic). - The choices available depend on your version of pywinrm type: list elements: string @@ -288,12 +291,11 @@ # calculate transport if needed if self._winrm_transport is None or self._winrm_transport[0] is None: # TODO: figure out what we want to do with auto-transport selection in the face of NTLM/Kerb/CredSSP/Cert/Basic - transport_selector = ['ssl'] if self._winrm_scheme == 'https' else ['plaintext'] - - if HAVE_KERBEROS and ((self._winrm_user and '@' in self._winrm_user)): - self._winrm_transport = ['kerberos'] + transport_selector + if self._winrm_user and '@' in self._winrm_user: + # A UPN must be a domain account and we always default to Kerberos for this. + self._winrm_transport = ['kerberos'] else: - self._winrm_transport = transport_selector + self._winrm_transport = ['ssl'] if self._winrm_scheme == 'https' else ['plaintext'] unsupported_transports = set(self._winrm_transport).difference(self._winrm_supported_authtypes) @@ -418,7 +420,12 @@ for transport in self._winrm_transport: if transport == 'kerberos': if not HAVE_KERBEROS: - errors.append('kerberos: the python kerberos library is not installed') + kerb_msg = ( + 'WinRM Kerberos authentication requested but the python kerberos library is not installed. ' + 'Please install the pykerberos library, set a different authentication method with ansible_winrm_transport, ' + 'or use a local user account to connect using basic authentication.' + ) + errors.append(kerb_msg) continue if self._kerb_managed: self._kerb_auth(self._winrm_user, self._winrm_pass) @@ -618,11 +625,16 @@ stdout = to_text(b_stdout) stderr = to_text(b_stderr) + log_stdout = stdout + log_stderr = stderr + if self._play_context.no_log: + log_stdout = log_stderr = '' + if from_exec: - display.vvvvv('WINRM RESULT ' % (rc, stdout, stderr), host=self._winrm_host) + display.vvvvv(f'WINRM RESULT ', host=self._winrm_host) display.vvvvvv('WINRM RC %d' % rc, host=self._winrm_host) - display.vvvvvv('WINRM STDOUT %s' % stdout, host=self._winrm_host) - display.vvvvvv('WINRM STDERR %s' % stderr, host=self._winrm_host) + display.vvvvvv(f'WINRM STDOUT {log_stdout}', host=self._winrm_host) + display.vvvvvv(f'WINRM STDERR {log_stderr}', host=self._winrm_host) # This is done after logging so we can still see the raw stderr for # debugging purposes. diff -Nru ansible-core-2.19.4/lib/ansible/plugins/lookup/config.py ansible-core-2.19.11/lib/ansible/plugins/lookup/config.py --- ansible-core-2.19.4/lib/ansible/plugins/lookup/config.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/lib/ansible/plugins/lookup/config.py 2026-06-18 19:34:02.000000000 +0000 @@ -80,6 +80,8 @@ type: raw """ +from collections import ChainMap + import ansible.plugins.loader as plugin_loader from ansible import constants as C @@ -104,6 +106,9 @@ ret = [] + # primarily use task vars, but fallback to existing constants when needed + var_context = ChainMap(variables, vars(C)) + for term in terms: if not isinstance(term, str): raise AnsibleError(f'Invalid setting identifier, {term!r} is not a {str}, its a {type(term)}.') @@ -119,7 +124,7 @@ if p is None: raise AnsibleError(f"Unable to load {ptype} plugin {pname!r}.") try: - result, origin = C.config.get_config_value_and_origin(term, plugin_type=ptype, plugin_name=pname, variables=variables) + result, origin = C.config.get_config_value_and_origin(term, plugin_type=ptype, plugin_name=pname, variables=var_context) except AnsibleUndefinedConfigEntry as e: match missing: case 'error': diff -Nru ansible-core-2.19.4/lib/ansible/plugins/lookup/first_found.py ansible-core-2.19.11/lib/ansible/plugins/lookup/first_found.py --- ansible-core-2.19.4/lib/ansible/plugins/lookup/first_found.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/lib/ansible/plugins/lookup/first_found.py 2026-06-18 19:34:02.000000000 +0000 @@ -79,7 +79,7 @@ - name: Include tasks only if one of the files exists, otherwise skip ansible.builtin.include_tasks: '{{ tasks_file }}' - when: tasks_file != "" + when: tasks_file is not none vars: tasks_file: "{{ lookup('ansible.builtin.first_found', files=['tasks.yaml', 'other_tasks.yaml'], errors='ignore') }}" diff -Nru ansible-core-2.19.4/lib/ansible/plugins/vars/host_group_vars.py ansible-core-2.19.11/lib/ansible/plugins/vars/host_group_vars.py --- ansible-core-2.19.4/lib/ansible/plugins/vars/host_group_vars.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/lib/ansible/plugins/vars/host_group_vars.py 2026-06-18 19:34:02.000000000 +0000 @@ -53,7 +53,7 @@ """ import os -from ansible.errors import AnsibleParserError +from ansible.errors import AnsibleError, AnsibleParserError from ansible.module_utils.common.text.converters import to_native from ansible.plugins.vars import BaseVarsPlugin from ansible.utils.path import basedir @@ -74,7 +74,10 @@ for found in found_files: new_data = loader.load_from_file(found, cache='all', unsafe=True, trusted_as_template=True) if new_data: # ignore empty files - data = combine_vars(data, new_data) + try: + data = combine_vars(data, new_data) + except AnsibleError as e: + raise AnsibleParserError(f"Could not process {found!r}.") from e return data def get_vars(self, loader, path, entities, cache=True): diff -Nru ansible-core-2.19.4/lib/ansible/release.py ansible-core-2.19.11/lib/ansible/release.py --- ansible-core-2.19.4/lib/ansible/release.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/lib/ansible/release.py 2026-06-18 19:34:02.000000000 +0000 @@ -17,6 +17,6 @@ from __future__ import annotations -__version__ = '2.19.4' +__version__ = '2.19.11' __author__ = 'Ansible, Inc.' __codename__ = "What Is and What Should Never Be" diff -Nru ansible-core-2.19.4/lib/ansible/utils/galaxy.py ansible-core-2.19.11/lib/ansible/utils/galaxy.py --- ansible-core-2.19.4/lib/ansible/utils/galaxy.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/lib/ansible/utils/galaxy.py 2026-06-18 19:34:02.000000000 +0000 @@ -72,7 +72,7 @@ elif scm == 'hg': clone_cmd.append('--insecure') - clone_cmd.extend([src, name]) + clone_cmd.extend(['--', src, name]) run_scm_cmd(clone_cmd, tempdir) diff -Nru ansible-core-2.19.4/lib/ansible/utils/jsonrpc.py ansible-core-2.19.11/lib/ansible/utils/jsonrpc.py --- ansible-core-2.19.4/lib/ansible/utils/jsonrpc.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/lib/ansible/utils/jsonrpc.py 2026-06-18 19:34:02.000000000 +0000 @@ -11,6 +11,7 @@ from ansible.module_utils.connection import ConnectionError from ansible.module_utils.six import binary_type, text_type from ansible.utils.display import Display +from ansible.utils.vars import transform_to_native_types display = Display() @@ -83,7 +84,8 @@ result = to_text(result) if not isinstance(result, text_type): response["result_type"] = "pickle" - result = to_text(pickle.dumps(result), errors='surrogateescape') + # typically consumed in a module context; transform custom types (e.g. tagged/vaulted values) to native to prevent unpickling failures + result = to_text(pickle.dumps(transform_to_native_types(result, redact=False))) response['result'] = result return response diff -Nru ansible-core-2.19.4/lib/ansible/utils/vars.py ansible-core-2.19.11/lib/ansible/utils/vars.py --- ansible-core-2.19.4/lib/ansible/utils/vars.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/lib/ansible/utils/vars.py 2026-06-18 19:34:02.000000000 +0000 @@ -72,9 +72,7 @@ myvars.append(dumps(x)) except Exception: myvars.append(to_native(x)) - raise AnsibleError("failed to combine variables, expected dicts but got a '{0}' and a '{1}': \n{2}\n{3}".format( - a.__class__.__name__, b.__class__.__name__, myvars[0], myvars[1]) - ) + raise AnsibleError(f"failed to combine variables, expected dicts but got a '{a.__class__.__name__}' and a '{b.__class__.__name__}'.") def combine_vars(a, b, merge=None): diff -Nru ansible-core-2.19.4/lib/ansible/vars/manager.py ansible-core-2.19.11/lib/ansible/vars/manager.py --- ansible-core-2.19.4/lib/ansible/vars/manager.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/lib/ansible/vars/manager.py 2026-06-18 19:34:02.000000000 +0000 @@ -297,7 +297,7 @@ # push facts to main namespace if C.INJECT_FACTS_AS_VARS: - deprecated_facts_vars = {k: _deprecate_top_level_fact(v) for k, v in clean_facts(facts).items()} + deprecated_facts_vars = {k: (_deprecate_top_level_fact(v) if k != 'ansible_local' else v) for k, v in clean_facts(facts).items()} all_vars = _combine_and_track(all_vars, deprecated_facts_vars, "facts") else: # always 'promote' ansible_local @@ -345,18 +345,16 @@ data = preprocess_vars(self._loader.load_from_file(found_file, unsafe=True, cache='vaulted', trusted_as_template=True)) if data is not None: for item in data: - all_vars = _combine_and_track(all_vars, item, "play vars_files from '%s'" % vars_file) + all_vars = _combine_and_track(all_vars, item, f"play vars_files from {vars_file!r}") display.vvv(f"Read `vars_file` {found_file!r}.") break except AnsibleFileNotFound: # we continue on loader failures continue - except AnsibleParserError: + except (AnsibleParserError, AnsibleUndefinedVariable): raise - except AnsibleUndefinedVariable: - raise - except Exception as ex: - raise AnsibleParserError(f"Error reading `vars_files` file {vars_file!r}.", obj=vars_file) from ex + except AnsibleError as e: + raise AnsibleError(f"Invalid vars_files file {found_file!r}.") from e except AnsibleUndefinedVariable as ex: if host is not None: @@ -369,7 +367,7 @@ raise AnsibleUndefinedVariable("an undefined variable was found when attempting to template the vars_files item '%s'" % vars_file_item, obj=vars_file_item) from ex - display.warning("skipping vars_file item due to an undefined variable", obj=vars_file_item) + display.warning("skipping vars_files item due to an undefined variable", obj=vars_file_item) continue # We now merge in all exported vars from all roles in the play (very high precedence) diff -Nru ansible-core-2.19.4/pyproject.toml ansible-core-2.19.11/pyproject.toml --- ansible-core-2.19.4/pyproject.toml 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/pyproject.toml 2026-06-18 19:34:02.000000000 +0000 @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools >= 66.1.0, <= 80.9.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, <= 82.0.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 -Nru ansible-core-2.19.4/test/integration/targets/ansible-galaxy-role/tasks/git-config-injection.yml ansible-core-2.19.11/test/integration/targets/ansible-galaxy-role/tasks/git-config-injection.yml --- ansible-core-2.19.4/test/integration/targets/ansible-galaxy-role/tasks/git-config-injection.yml 1970-01-01 00:00:00.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/ansible-galaxy-role/tasks/git-config-injection.yml 2026-06-18 19:34:02.000000000 +0000 @@ -0,0 +1,52 @@ +- vars: + invalid_git_opts: '-ccore.sshCommand=sh -c "id > {{ remote_tmp_dir }}/role_exe"' + # use SSH protocol to test core.sshCommand is not configured + dummy_repo: git@github.com:ansible/nosuchrepo.git + block: + - name: Ensure git is installed + package: + name: git + when: ansible_distribution not in ["MacOSX", "Alpine"] + register: git_install + + - name: Create invalid requirements file + copy: + dest: "{{ remote_tmp_dir }}/invalid-requirements.yml" + content: | + - src: {{ invalid_git_opts }} + scm: git + name: {{ dummy_repo }} + - src: {{ dummy_repo }} + scm: git + name: {{ invalid_git_opts }} + + - name: Attempt to install invalid role requirements + command: ansible-galaxy install -r {{ remote_tmp_dir }}/invalid-requirements.yml --ignore-errors + register: result + environment: + ANSIBLE_NOCOLOR: True + ANSIBLE_FORCE_COLOR: False + + - name: Validate git core.sshCommand did not run + stat: + path: "{{ remote_tmp_dir }}/role_exe" + register: stat_result + failed_when: stat_result.stat.exists + + - name: Verify the invalid field is treated as a single positional argument (repo or dest) + assert: + that: + - stderr is search(error1) + - stderr is search(error2) + - (stderr | regex_findall("git clone") | length) == (stderr | regex_findall("git clone --") | length) == 2 + vars: + stderr: "{{ result.stderr | regex_replace('\\n', ' ') }}" + error1: "repository '{{ invalid_git_opts }}' does not exist" + error2: "Cloning into '{{ invalid_git_opts }}'" + + always: + - name: Uninstall git if it was installed + package: + name: git + state: absent + when: git_install is changed | default(false) diff -Nru ansible-core-2.19.4/test/integration/targets/ansible-galaxy-role/tasks/main.yml ansible-core-2.19.11/test/integration/targets/ansible-galaxy-role/tasks/main.yml --- ansible-core-2.19.4/test/integration/targets/ansible-galaxy-role/tasks/main.yml 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/ansible-galaxy-role/tasks/main.yml 2026-06-18 19:34:02.000000000 +0000 @@ -70,3 +70,4 @@ - import_tasks: dir-traversal.yml - import_tasks: valid-role-symlinks.yml +- import_tasks: git-config-injection.yml diff -Nru ansible-core-2.19.4/test/integration/targets/ansible-test-cloud-azure/aliases ansible-core-2.19.11/test/integration/targets/ansible-test-cloud-azure/aliases --- ansible-core-2.19.4/test/integration/targets/ansible-test-cloud-azure/aliases 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/ansible-test-cloud-azure/aliases 2026-06-18 19:34:02.000000000 +0000 @@ -1,3 +1,4 @@ cloud/azure shippable/generic/group1 context/controller +disabled/yes # Azure credential provisioner pool broken in Core CI (likely SP policy) diff -Nru ansible-core-2.19.4/test/integration/targets/ansible-test-metadata/aliases ansible-core-2.19.11/test/integration/targets/ansible-test-metadata/aliases --- ansible-core-2.19.4/test/integration/targets/ansible-test-metadata/aliases 1970-01-01 00:00:00.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/ansible-test-metadata/aliases 2026-06-18 19:34:02.000000000 +0000 @@ -0,0 +1,3 @@ +shippable/posix/group3 # runs in the distro test containers +shippable/generic/group1 # runs in the default test container +context/controller diff -Nru ansible-core-2.19.4/test/integration/targets/ansible-test-metadata/runme.sh ansible-core-2.19.11/test/integration/targets/ansible-test-metadata/runme.sh --- ansible-core-2.19.4/test/integration/targets/ansible-test-metadata/runme.sh 1970-01-01 00:00:00.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/ansible-test-metadata/runme.sh 2026-06-18 19:34:02.000000000 +0000 @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Verify that importlib.metadata can find ansible-core using the PYTHONPATH set by ansible-test. +# Regression test for https://github.com/ansible/ansible/issues/86695 + +set -eux + +VERSION=$(python -c "from importlib.metadata import version; print(version('ansible-core'))") + +test "$VERSION" = "$ANSIBLE_TEST_ANSIBLE_VERSION" diff -Nru ansible-core-2.19.4/test/integration/targets/ansible-test-sanity-runtime-metadata/ansible_collections/ns/prerelease/galaxy.yml ansible-core-2.19.11/test/integration/targets/ansible-test-sanity-runtime-metadata/ansible_collections/ns/prerelease/galaxy.yml --- ansible-core-2.19.4/test/integration/targets/ansible-test-sanity-runtime-metadata/ansible_collections/ns/prerelease/galaxy.yml 1970-01-01 00:00:00.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/ansible-test-sanity-runtime-metadata/ansible_collections/ns/prerelease/galaxy.yml 2026-06-18 19:34:02.000000000 +0000 @@ -0,0 +1,5 @@ +namespace: ns +name: prerelease +version: 3.0.0-prerelease +authors: + - Ansible diff -Nru ansible-core-2.19.4/test/integration/targets/ansible-test-sanity-runtime-metadata/ansible_collections/ns/prerelease/meta/runtime.yml ansible-core-2.19.11/test/integration/targets/ansible-test-sanity-runtime-metadata/ansible_collections/ns/prerelease/meta/runtime.yml --- ansible-core-2.19.4/test/integration/targets/ansible-test-sanity-runtime-metadata/ansible_collections/ns/prerelease/meta/runtime.yml 1970-01-01 00:00:00.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/ansible-test-sanity-runtime-metadata/ansible_collections/ns/prerelease/meta/runtime.yml 2026-06-18 19:34:02.000000000 +0000 @@ -0,0 +1,25 @@ +plugin_routing: + modules: + deprecated_module: + deprecation: + removal_version: 4.0.0 + warning_text: Will no longer be there. + tombstoned_module: + tombstone: + removal_version: 3.0.0 + warning_text: Is no longer there. + tombstoned_module_with_prerelease_version: + tombstone: + removal_version: 3.0.0-a1 + warning_text: Is no longer there. + tombstoned_module_with_prerelease_and_build_meta_version: + tombstone: + removal_version: 3.0.0-a1+bla.test.1234567 + warning_text: Is no longer there. + tombstoned_module_with_build_meta_version: + tombstone: + removal_version: 3.0.0+bla.test.1234567 + warning_text: Is no longer there. + module_with_invalid_removal_version: + tombstone: + removal_version: 4.0.0 diff -Nru ansible-core-2.19.4/test/integration/targets/ansible-test-sanity-runtime-metadata/expected-prerelease.txt ansible-core-2.19.11/test/integration/targets/ansible-test-sanity-runtime-metadata/expected-prerelease.txt --- ansible-core-2.19.4/test/integration/targets/ansible-test-sanity-runtime-metadata/expected-prerelease.txt 1970-01-01 00:00:00.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/ansible-test-sanity-runtime-metadata/expected-prerelease.txt 2026-06-18 19:34:02.000000000 +0000 @@ -0,0 +1 @@ +meta/runtime.yml:0:0: The tombstone removal_version ('4.0.0') must not be after the current version (SemanticVersion('3.0.0')) for dictionary value @ data['plugin_routing']['modules']['module_with_invalid_removal_version']['tombstone']['removal_version']. Got '4.0.0' diff -Nru ansible-core-2.19.4/test/integration/targets/ansible-test-sanity-runtime-metadata/runme.sh ansible-core-2.19.11/test/integration/targets/ansible-test-sanity-runtime-metadata/runme.sh --- ansible-core-2.19.4/test/integration/targets/ansible-test-sanity-runtime-metadata/runme.sh 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/ansible-test-sanity-runtime-metadata/runme.sh 2026-06-18 19:34:02.000000000 +0000 @@ -13,3 +13,8 @@ ansible-test sanity --test runtime-metadata --color --truncate 0 --failure-ok --lint "${@}" 1> actual-stdout.txt 2> actual-stderr.txt diff -u "${TEST_DIR}/expected-no_version.txt" actual-stdout.txt grep -F -f "${TEST_DIR}/expected-no_version.txt" actual-stderr.txt + +cd ../prerelease +ansible-test sanity --test runtime-metadata --color --truncate 0 --failure-ok --lint "${@}" 1> actual-stdout.txt 2> actual-stderr.txt +diff -u "${TEST_DIR}/expected-prerelease.txt" actual-stdout.txt +grep -F -f "${TEST_DIR}/expected-prerelease.txt" actual-stderr.txt diff -Nru ansible-core-2.19.4/test/integration/targets/become_su/aliases ansible-core-2.19.11/test/integration/targets/become_su/aliases --- ansible-core-2.19.4/test/integration/targets/become_su/aliases 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/become_su/aliases 2026-06-18 19:34:02.000000000 +0000 @@ -5,6 +5,7 @@ needs/root needs/target/setup_become_user_pair needs/target/setup_test_user +needs/target/setup_pexpect setup/always/setup_passlib_controller # required for setup_test_user skip/macos # requires a TTY skip/freebsd # appears to require a TTY (ignores password input from stdin) diff -Nru ansible-core-2.19.4/test/integration/targets/become_su/tasks/main.yml ansible-core-2.19.11/test/integration/targets/become_su/tasks/main.yml --- ansible-core-2.19.4/test/integration/targets/become_su/tasks/main.yml 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/become_su/tasks/main.yml 2026-06-18 19:34:02.000000000 +0000 @@ -61,3 +61,46 @@ - wrong_su_prompt is unreachable - ansible_connection != "local" or wrong_su_prompt.msg is contains "Timed out waiting for become success or become password prompt" - ansible_connection != "ssh" or wrong_su_prompt.msg is contains "waiting for privilege escalation prompt" + +# The bug from issue https://github.com/ansible/ansible/issues/86458 presents itself only on local connections +# and only when the password is entered interactively using -K/--ask-become-pass. +- name: Test different locale + when: ansible_os_family == 'RedHat' and ansible_connection == 'local' + block: + - import_role: + name: setup_pexpect + + - name: install language pack and pexpect + dnf: + name: + - langpacks-uk + - glibc-langpack-uk + state: present + + - name: verify language pack is enabled + command: locale -a + register: installed_languages + + - assert: + that: "'uk_UA.utf8' in installed_languages.stdout" + + - name: test su prompt basic success with alternate language on local connection + expect: + command: "ansible -m command -a whoami localhost -b --become-method su -K" + responses: + BECOME password: + - "{{ target_user_password }}" + environment: + LC_ALL: uk_UA.utf8 + ANSIBLE_BECOME_USER: "{{ target_user_name }}" + ANSIBLE_BECOME_EXE: /tmp/sushim.sh + ANSIBLE_BECOME_FLAGS: --intermediate-user {{ intermediate_user_name | quote }} + # protect our target user password + no_log: true + always: + - name: remove language pack + dnf: + name: + - langpacks-uk + - glibc-langpack-uk + state: absent diff -Nru ansible-core-2.19.4/test/integration/targets/deprecations/injectfacts.yml ansible-core-2.19.11/test/integration/targets/deprecations/injectfacts.yml --- ansible-core-2.19.4/test/integration/targets/deprecations/injectfacts.yml 1970-01-01 00:00:00.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/deprecations/injectfacts.yml 2026-06-18 19:34:02.000000000 +0000 @@ -0,0 +1,8 @@ +- hosts: localhost + gather_facts: true + tasks: + - debug: + msg: '{{ansible_distribution}}' + - debug: + msg: '{{ansible_local}}' + tags: alocal diff -Nru ansible-core-2.19.4/test/integration/targets/deprecations/runme.sh ansible-core-2.19.11/test/integration/targets/deprecations/runme.sh --- ansible-core-2.19.4/test/integration/targets/deprecations/runme.sh 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/deprecations/runme.sh 2026-06-18 19:34:02.000000000 +0000 @@ -56,3 +56,9 @@ # check for plugin deprecation [ "$(ansible-doc -t cache notjsonfile --playbook-dir ./ | grep -c 'DEPRECATED:')" -eq "1" ] + +# Injection default is deprecated +[ "$(ANSIBLE_INJECT_FACT_VARS=1 ansible-playbook injectfacts.yml 2>&1 | grep -c 'INJECT_FACTS_AS_VARS')" -eq "0" ] + +# Injection default is deprecated but not ansible_local +[ "$(ansible-playbook injectfacts.yml --tags alocal 2>&1 | grep -c 'INJECT_FACTS_AS_VARS')" -eq "0" ] diff -Nru ansible-core-2.19.4/test/integration/targets/gathering_facts/runme.sh ansible-core-2.19.11/test/integration/targets/gathering_facts/runme.sh --- ansible-core-2.19.4/test/integration/targets/gathering_facts/runme.sh 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/gathering_facts/runme.sh 2026-06-18 19:34:02.000000000 +0000 @@ -26,6 +26,9 @@ ansible-playbook test_module_defaults.yml "$@" --tags networking +# test gather_facts action templates module_defaults for different module name +ansible-playbook test_module_defaults.yml "$@" --tags templating + # test it works by default ANSIBLE_FACTS_MODULES='ansible.legacy.slow' ansible -m gather_facts localhost --playbook-dir ./ "$@" diff -Nru ansible-core-2.19.4/test/integration/targets/gathering_facts/test_module_defaults.yml ansible-core-2.19.11/test/integration/targets/gathering_facts/test_module_defaults.yml --- ansible-core-2.19.4/test/integration/targets/gathering_facts/test_module_defaults.yml 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/gathering_facts/test_module_defaults.yml 2026-06-18 19:34:02.000000000 +0000 @@ -128,3 +128,21 @@ - assert: that: - "ansible_facts.gather_subset == 'min'" + +- name: Test module_defaults templating when the action and module differ + hosts: localhost + gather_facts: True + tags: + - templating + vars: + subset_facts: + - "!all" + - "!min" + - env + module_defaults: + setup: + gather_subset: "{{ subset_facts }}" + tasks: + - assert: + that: + - ansible_facts.gather_subset == subset_facts diff -Nru ansible-core-2.19.4/test/integration/targets/get_url/tasks/main.yml ansible-core-2.19.11/test/integration/targets/get_url/tasks/main.yml --- ansible-core-2.19.4/test/integration/targets/get_url/tasks/main.yml 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/get_url/tasks/main.yml 2026-06-18 19:34:02.000000000 +0000 @@ -1,20 +1,6 @@ # Test code for the get_url module # (c) 2014, Richard Isaacson - -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - name: Determine if python looks like it will support modern ssl features like SNI command: "{{ ansible_python.executable }} -c 'from ssl import SSLContext'" @@ -332,13 +318,13 @@ - name: create src file copy: - dest: '{{ files_dir }}/27617.txt' - content: "ptux" - -- name: create duplicate src file - copy: - dest: '{{ files_dir }}/71420.txt' + dest: '{{ files_dir }}/{{ item }}.txt' content: "ptux" + loop: + - 27617 + - 71420 + - 86132 + - 86132_single_space - name: create sha1 checksum file of src copy: @@ -346,6 +332,8 @@ content: | a97e6837f60cec6da4491bab387296bbcd72bdba 27617.txt a97e6837f60cec6da4491bab387296bbcd72bdba 71420.txt + a97e6837f60cec6da4491bab387296bbcd72bdba 86132.txt + a97e6837f60cec6da4491bab387296bbcd72bdba 86132_single_space.txt 3911340502960ca33aece01129234460bfeb2791 not_target1.txt 1b4b6adf30992cedb0f6edefd6478ff0a593b2e4 not_target2.txt @@ -355,6 +343,8 @@ content: | b1b6ce5073c8fac263a8fc5edfffdbd5dec1980c784e09c5bc69f8fb6056f006. 27617.txt b1b6ce5073c8fac263a8fc5edfffdbd5dec1980c784e09c5bc69f8fb6056f006. 71420.txt + b1b6ce5073c8fac263a8fc5edfffdbd5dec1980c784e09c5bc69f8fb6056f006. 86132.txt + b1b6ce5073c8fac263a8fc5edfffdbd5dec1980c784e09c5bc69f8fb6056f006. 86132_single_space.txt 30949cc401e30ac494d695ab8764a9f76aae17c5d73c67f65e9b558f47eff892 not_target1.txt d0dbfc1945bc83bf6606b770e442035f2c4e15c886ee0c22fb3901ba19900b5b not_target2.txt @@ -364,6 +354,8 @@ content: | b1b6ce5073c8fac263a8fc5edfffdbd5dec1980c784e09c5bc69f8fb6056f006. ./27617.txt b1b6ce5073c8fac263a8fc5edfffdbd5dec1980c784e09c5bc69f8fb6056f006. ./71420.txt + b1b6ce5073c8fac263a8fc5edfffdbd5dec1980c784e09c5bc69f8fb6056f006. ./86132.txt + b1b6ce5073c8fac263a8fc5edfffdbd5dec1980c784e09c5bc69f8fb6056f006. ./86132_single_space.txt 30949cc401e30ac494d695ab8764a9f76aae17c5d73c67f65e9b558f47eff892 ./not_target1.txt d0dbfc1945bc83bf6606b770e442035f2c4e15c886ee0c22fb3901ba19900b5b ./not_target2.txt @@ -373,6 +365,8 @@ content: | b1b6ce5073c8fac263a8fc5edfffdbd5dec1980c784e09c5bc69f8fb6056f006. *27617.txt b1b6ce5073c8fac263a8fc5edfffdbd5dec1980c784e09c5bc69f8fb6056f006. *71420.txt + b1b6ce5073c8fac263a8fc5edfffdbd5dec1980c784e09c5bc69f8fb6056f006. *86132.txt + b1b6ce5073c8fac263a8fc5edfffdbd5dec1980c784e09c5bc69f8fb6056f006. *86132_single_space.txt 30949cc401e30ac494d695ab8764a9f76aae17c5d73c67f65e9b558f47eff892 *not_target1.txt d0dbfc1945bc83bf6606b770e442035f2c4e15c886ee0c22fb3901ba19900b5b *not_target2.txt @@ -551,6 +545,61 @@ path: "{{ remote_tmp_dir }}/27617.txt" register: stat_result_sha256_checksum_only +- name: download 86132.txt with sha1 checksum url + get_url: + url: 'http://localhost:{{ http_port }}/86132.txt' + dest: '{{ remote_tmp_dir }}' + checksum: 'sha1:http://localhost:{{ http_port }}/sha1sum.txt' + register: result_sha1_86132 + +- stat: + path: "{{ remote_tmp_dir }}/86132.txt" + register: stat_result_sha1_86132 + +- name: download 86132.txt with sha256 checksum url + get_url: + url: 'http://localhost:{{ http_port }}/86132.txt' + dest: '{{ remote_tmp_dir }}/86132sha256.txt' + checksum: 'sha256:http://localhost:{{ http_port }}/sha256sum.txt' + register: result_sha256_86132 + +- stat: + path: "{{ remote_tmp_dir }}/86132.txt" + register: stat_result_sha256_86132 + +- name: download 86132.txt with sha256 checksum url with dot leading paths + get_url: + url: 'http://localhost:{{ http_port }}/86132.txt' + dest: '{{ remote_tmp_dir }}/86132sha256_with_dot.txt' + checksum: 'sha256:http://localhost:{{ http_port }}/sha256sum_with_dot.txt' + register: result_sha256_with_dot_86132 + +- stat: + path: "{{ remote_tmp_dir }}/86132sha256_with_dot.txt" + register: stat_result_sha256_with_dot_86132 + +- name: download 86132.txt with sha256 checksum url with asterisk leading paths + get_url: + url: 'http://localhost:{{ http_port }}/86132.txt' + dest: '{{ remote_tmp_dir }}/86132sha256_with_asterisk.txt' + checksum: 'sha256:http://localhost:{{ http_port }}/sha256sum_with_asterisk.txt' + register: result_sha256_with_asterisk_86132 + +- stat: + path: "{{ remote_tmp_dir }}/86132sha256_with_asterisk.txt" + register: stat_result_sha256_with_asterisk_86132 + +- name: download 86132_single_space.txt with sha256 checksum url + get_url: + url: 'http://localhost:{{ http_port }}/86132_single_space.txt' + dest: '{{ remote_tmp_dir }}/86132_single_space.txt' + checksum: 'sha256:http://localhost:{{ http_port }}/sha256sum.txt' + register: result_sha256_86132_single_space + +- stat: + path: "{{ remote_tmp_dir }}/86132_single_space.txt" + register: stat_result_sha256_86132_single_space + - name: Assert that the file was downloaded assert: that: @@ -579,6 +628,16 @@ - "stat_result_sha256_with_asterisk_71420.stat.exists == true" - "stat_result_sha256_with_file_scheme_71420.stat.exists == true" - "stat_result_sha256_checksum_only.stat.exists == true" + - result_sha1_86132 is changed + - result_sha256_86132 is changed + - result_sha256_with_dot_86132 is changed + - result_sha256_with_asterisk_86132 is changed + - "stat_result_sha1_86132.stat.exists == true" + - "stat_result_sha256_86132.stat.exists == true" + - "stat_result_sha256_with_dot_86132.stat.exists == true" + - "stat_result_sha256_with_asterisk_86132.stat.exists == true" + - result_sha256_86132_single_space is changed + - "stat_result_sha256_86132_single_space.stat.exists == true" - name: Test for incomplete data read (issue 85164) get_url: diff -Nru ansible-core-2.19.4/test/integration/targets/iptables/tasks/main.yml ansible-core-2.19.11/test/integration/targets/iptables/tasks/main.yml --- ansible-core-2.19.4/test/integration/targets/iptables/tasks/main.yml 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/iptables/tasks/main.yml 2026-06-18 19:34:02.000000000 +0000 @@ -35,4 +35,14 @@ # prevent attempts to upgrade the kernel and install kernel modules for a non-running kernel version exclude: "{{ 'kernel-core' if ansible_distribution == 'RedHat' else omit }}" +- name: install xt_comment for iptables `-m comment` tests on RHEL 10 + dnf: + name: + - kernel-modules-extra-{{ ansible_facts.kernel }} + state: present + exclude: + # prevent attempts to upgrade the kernel and install kernel modules for a non-running kernel version + - kernel-core + when: ansible_distribution == 'RedHat' + - import_tasks: chain_management.yml diff -Nru ansible-core-2.19.4/test/integration/targets/lookup_config/tasks/main.yml ansible-core-2.19.11/test/integration/targets/lookup_config/tasks/main.yml --- ansible-core-2.19.4/test/integration/targets/lookup_config/tasks/main.yml 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/lookup_config/tasks/main.yml 2026-06-18 19:34:02.000000000 +0000 @@ -129,3 +129,8 @@ - config_origin1[1] == "default" - config_origin2[0] == 'yolo' - 'config_origin2[1] == "var: _z_test_entry"' + +- name: verify interdependent templating + assert: + that: + - lookup('config', 'ANSIBLE_HOME') == lookup('config', '_Z_TEST_ENTRY_3') \ No newline at end of file diff -Nru ansible-core-2.19.4/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/meta/runtime.yml ansible-core-2.19.11/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/meta/runtime.yml --- ansible-core-2.19.4/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/meta/runtime.yml 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/meta/runtime.yml 2026-06-18 19:34:02.000000000 +0000 @@ -69,6 +69,8 @@ - metadata: extend_group: - testgroup + contains_setup: + - ansible.builtin.setup empty_metadata: - metadata: {} bad_metadata_format: diff -Nru ansible-core-2.19.4/test/integration/targets/module_defaults/test_action_groups.yml ansible-core-2.19.11/test/integration/targets/module_defaults/test_action_groups.yml --- ansible-core-2.19.4/test/integration/targets/module_defaults/test_action_groups.yml 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/module_defaults/test_action_groups.yml 2026-06-18 19:34:02.000000000 +0000 @@ -130,3 +130,18 @@ - metadata: collections: - testns.testcoll + + - name: Test action plugin templates group defaults + vars: + setup_subset: + - "!all" + - "!min" + - local + module_defaults: + group/testns.testcoll.contains_setup: + gather_subset: "{{ setup_subset }}" + block: + - gather_facts: + - assert: + that: + - ansible_facts.gather_subset == setup_subset diff -Nru ansible-core-2.19.4/test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/library/add_type_test.ps1 ansible-core-2.19.11/test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/library/add_type_test.ps1 --- ansible-core-2.19.4/test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/library/add_type_test.ps1 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/library/add_type_test.ps1 2026-06-18 19:34:02.000000000 +0000 @@ -322,6 +322,7 @@ $env:LIB = "C:\fake\folder\path" try { Add-CSharpType -Reference $lib_set + Assert-Equal -actual $env:LIB -expected "C:\fake\folder\path" } finally { Remove-Item -LiteralPath env:\LIB diff -Nru ansible-core-2.19.4/test/integration/targets/no_log/library/module.py ansible-core-2.19.11/test/integration/targets/no_log/library/module.py --- ansible-core-2.19.4/test/integration/targets/no_log/library/module.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/no_log/library/module.py 2026-06-18 19:34:02.000000000 +0000 @@ -37,7 +37,7 @@ } ) - module.exit_json(msg='done') + module.exit_json(msg='done', values=', '.join([str(v) for v in module.params.values() if v])) if __name__ == '__main__': diff -Nru ansible-core-2.19.4/test/integration/targets/no_log/runme.sh ansible-core-2.19.11/test/integration/targets/no_log/runme.sh --- ansible-core-2.19.4/test/integration/targets/no_log/runme.sh 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/no_log/runme.sh 2026-06-18 19:34:02.000000000 +0000 @@ -30,3 +30,6 @@ [ "$(ansible-playbook no_log_config.yml -i ../../inventory -vvvvv "$@" | grep -Ec 'the output has been hidden')" = "1" ] [ "$(ANSIBLE_NO_LOG=0 ansible-playbook no_log_config.yml -i ../../inventory -vvvvv "$@" | grep -Ec 'the output has been hidden')" = "1" ] [ "$(ANSIBLE_NO_LOG=1 ansible-playbook no_log_config.yml -i ../../inventory -vvvvv "$@" | grep -Ec 'the output has been hidden')" = "5" ] + +# Ensure module no_log masking handles secrets that are substrings of other secrets correctly +ansible-playbook sub_masking.yml -i ../../inventory -vvvvv "$@" diff -Nru ansible-core-2.19.4/test/integration/targets/no_log/secretvars.yml ansible-core-2.19.11/test/integration/targets/no_log/secretvars.yml --- ansible-core-2.19.4/test/integration/targets/no_log/secretvars.yml 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/no_log/secretvars.yml 2026-06-18 19:34:02.000000000 +0000 @@ -30,3 +30,6 @@ s211: SECRET211 s212: SECRET212 s213: SECRET213 + +# substring +sec: RET diff -Nru ansible-core-2.19.4/test/integration/targets/no_log/sub_masking.yml ansible-core-2.19.11/test/integration/targets/no_log/sub_masking.yml --- ansible-core-2.19.4/test/integration/targets/no_log/sub_masking.yml 1970-01-01 00:00:00.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/no_log/sub_masking.yml 2026-06-18 19:34:02.000000000 +0000 @@ -0,0 +1,60 @@ +- name: Ensure no_log obfuscation does not mask on substrings + hosts: all + vars_files: + - secretvars.yml + gather_facts: no + + tasks: + - name: test with option long + module: + secret: "{{ s101 }}" + subopt_dict: + str_sub_opt1: "{{ s102 }}" + nested_subopt: + n_subopt1: "{{ s103 }}" + subopt_list: + - subopt1: "{{sec}}" + register: long_first + + - name: Task with suboptions long + module: + secret: "{{ sec }}" + subopt_dict: + str_sub_opt1: '{{ s101 }}' + nested_subopt: + n_subopt1: "{{ s102 }}" + subopt_list: + - subopt1: "{{ s103 }}" + register: short_first + + - name: Task with suboptions long + module: + secret: "{{ s101 }}" + subopt_dict: + str_sub_opt1: '{{ sec }}' + nested_subopt: + n_subopt1: "{{ s102 }}" + subopt_list: + - subopt1: "{{ s103 }}" + register: middle_top + + - name: Task with suboptions long + module: + secret: "{{ s102}}" + subopt_dict: + str_sub_opt1: '{{ s102 }}' + nested_subopt: + n_subopt1: "{{ sec }}" + subopt_list: + - subopt1: "{{ s103 }}" + register: middle_bottom + + - name: check output + assert: + that: + - "'SEC' not in (q('vars', item)|to_json)" + loop: + - long_first + - short_first + - middle_top + - middle_bottom diff -Nru ansible-core-2.19.4/test/integration/targets/package/tasks/main.yml ansible-core-2.19.11/test/integration/targets/package/tasks/main.yml --- ansible-core-2.19.4/test/integration/targets/package/tasks/main.yml 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/package/tasks/main.yml 2026-06-18 19:34:02.000000000 +0000 @@ -69,28 +69,53 @@ # Verify module_defaults for package and the underlying module are utilized # Validates: https://github.com/ansible/ansible/issues/72918 - block: - # 'name' is required + # note: dnf module unexpectedly succeeds when no arguments are provided. + # Until the requires_one_of (?) bug is fixed, this test asserts changes are made to avoid false positives. - name: install apt with package defaults package: module_defaults: package: name: apt state: present + register: result + - assert: + that: result is changed + + - name: uninstall apt between tests + dnf5: + name: apt + state: absent + + # Validate package handles applying templated defaults for dnf + # https://github.com/ansible/ansible/issues/85848 - name: install apt with dnf defaults (auto) package: module_defaults: - dnf: - name: apt + dnf5: + name: "{{ 'apt' }}" state: present + register: result - - name: install apt with dnf defaults (use dnf) + - assert: + that: result is changed + + - name: uninstall apt between tests + dnf5: + name: apt + state: absent + + - name: install apt with dnf defaults (use dnf5) package: - use: dnf + use: dnf5 module_defaults: - dnf: + dnf5: name: apt state: present + register: result + + - assert: + that: result is changed always: - name: remove apt dnf: diff -Nru ansible-core-2.19.4/test/integration/targets/pyyaml/runme.sh ansible-core-2.19.11/test/integration/targets/pyyaml/runme.sh --- ansible-core-2.19.4/test/integration/targets/pyyaml/runme.sh 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/pyyaml/runme.sh 2026-06-18 19:34:02.000000000 +0000 @@ -4,8 +4,18 @@ source virtualenv.sh set +x +# Verify libyaml is in use. +ansible --version | tee /dev/stderr | grep 'with libyaml' + +# Run tests with libyaml. +ansible-playbook runme.yml "${@}" + # deps are already installed, using --no-deps to avoid re-installing them # Install PyYAML without libyaml to validate ansible can run PYYAML_FORCE_LIBYAML=0 pip install --no-binary PyYAML --ignore-installed --no-cache-dir --no-deps PyYAML +# Verify libyaml is not in use. ansible --version | tee /dev/stderr | grep 'without libyaml' + +# Run tests without libyaml. +ansible-playbook runme.yml "${@}" diff -Nru ansible-core-2.19.4/test/integration/targets/pyyaml/runme.yml ansible-core-2.19.11/test/integration/targets/pyyaml/runme.yml --- ansible-core-2.19.4/test/integration/targets/pyyaml/runme.yml 1970-01-01 00:00:00.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/pyyaml/runme.yml 2026-06-18 19:34:02.000000000 +0000 @@ -0,0 +1,9 @@ +- hosts: localhost + gather_facts: no + tasks: + - name: Read YAML from a string + assert: + that: + - "'1' | from_yaml == 1" + - "'[1]' | from_yaml == [1]" + - "'key: value' | from_yaml == {'key': 'value'}" diff -Nru ansible-core-2.19.4/test/integration/targets/rpm_key/tasks/main.yaml ansible-core-2.19.11/test/integration/targets/rpm_key/tasks/main.yaml --- ansible-core-2.19.4/test/integration/targets/rpm_key/tasks/main.yaml 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/rpm_key/tasks/main.yaml 2026-06-18 19:34:02.000000000 +0000 @@ -1,3 +1,7 @@ + - name: Skip RHEL 10.1 until rpm_key has been updated + meta: end_play + when: ansible_distribution == "RedHat" and ansible_distribution_version == "10.1" + - when: ansible_os_family == "RedHat" block: diff -Nru ansible-core-2.19.4/test/integration/targets/service/tasks/tests.yml ansible-core-2.19.11/test/integration/targets/service/tasks/tests.yml --- ansible-core-2.19.4/test/integration/targets/service/tasks/tests.yml 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/service/tasks/tests.yml 2026-06-18 19:34:02.000000000 +0000 @@ -32,10 +32,12 @@ when: "ansible_service_mgr in ['sysvinit', 'systemd']" module_defaults: sysvinit: - name: ansible_test + # Test the action plugin templates sysvinit defaults + # https://github.com/ansible/ansible/issues/85848 + name: "{{ 'ansible_test' }}" enabled: yes systemd: - name: ansible_test + name: "{{ 'ansible_test' }}" enabled: yes - name: assert that changes reported for check mode run diff -Nru ansible-core-2.19.4/test/integration/targets/uri/tasks/main.yml ansible-core-2.19.11/test/integration/targets/uri/tasks/main.yml --- ansible-core-2.19.4/test/integration/targets/uri/tasks/main.yml 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/uri/tasks/main.yml 2026-06-18 19:34:02.000000000 +0000 @@ -202,11 +202,11 @@ stat: path: '{{ item }}' loop: + - '{{ cafile_path.stdout_lines|default(["/_i_dont_exist_ca.pem"])|first }}' - /etc/ssl/certs/ca-bundle.crt - /etc/ssl/certs/ca-certificates.crt - /var/lib/ca-certificates/ca-bundle.pem - /usr/local/share/certs/ca-root-nss.crt - - '{{ cafile_path.stdout_lines|default(["/_i_dont_exist_ca.pem"])|first }}' - /etc/ssl/cert.pem register: ca_bundle_candidates diff -Nru ansible-core-2.19.4/test/integration/targets/var_blending/error_handling.yml ansible-core-2.19.11/test/integration/targets/var_blending/error_handling.yml --- ansible-core-2.19.4/test/integration/targets/var_blending/error_handling.yml 1970-01-01 00:00:00.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/var_blending/error_handling.yml 2026-06-18 19:34:02.000000000 +0000 @@ -0,0 +1,14 @@ +- hosts: all + name: test vault errors + gather_facts: false + tasks: + + - name: bad included vault + include_vars: + file: vars/bad_vault.yml + tags: includevault, never + no_log: false + + - name: Show bad vault contents ... if i get here, it was a good vault! + debug: + msg: "{{ test_password }} {{ jdbc_test_password }} {{ api_test_password }}" File /srv/release.debian.org/tmp/ZbgoIJvXc9/ansible-core-2.19.4/test/integration/targets/var_blending/group_vars/local is a regular file while file /srv/release.debian.org/tmp/7eMEruybYo/ansible-core-2.19.11/test/integration/targets/var_blending/group_vars/local is a directory diff -Nru ansible-core-2.19.4/test/integration/targets/var_blending/runme.sh ansible-core-2.19.11/test/integration/targets/var_blending/runme.sh --- ansible-core-2.19.4/test/integration/targets/var_blending/runme.sh 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/var_blending/runme.sh 2026-06-18 19:34:02.000000000 +0000 @@ -3,3 +3,12 @@ set -eux ansible-playbook test_var_blending.yml -i inventory -e @test_vars.yml -v "$@" + +# check bad vault file erros +[ "$(ansible-playbook error_handling.yml -i inventory --vault-password-file supersecretvaultsecret -e @vars/bad_vault.yml 2>&1 | grep -c 'dummy')" -eq "0" ] +[ "$(ansible-playbook error_handling.yml -i inventory --vault-password-file supersecretvaultsecret --tags includevault 2>&1 | grep -c 'dummy')" -eq "0" ] + +# setup group file for bad vault tests +trap 'rm group_vars/local/bad_vault.yml' EXIT +ln -s "${PWD}/vars/bad_vault.yml" group_vars/local/ +[ "$(ansible-playbook error_handling.yml -i inventory --vault-password-file supersecretvaultsecret 2>&1 | grep -c 'dummy')" -eq "0" ] diff -Nru ansible-core-2.19.4/test/integration/targets/var_blending/supersecretvaultsecret ansible-core-2.19.11/test/integration/targets/var_blending/supersecretvaultsecret --- ansible-core-2.19.4/test/integration/targets/var_blending/supersecretvaultsecret 1970-01-01 00:00:00.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/var_blending/supersecretvaultsecret 2026-06-18 19:34:02.000000000 +0000 @@ -0,0 +1 @@ +test diff -Nru ansible-core-2.19.4/test/integration/targets/var_blending/vars/bad_vault.yml ansible-core-2.19.11/test/integration/targets/var_blending/vars/bad_vault.yml --- ansible-core-2.19.4/test/integration/targets/var_blending/vars/bad_vault.yml 1970-01-01 00:00:00.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/var_blending/vars/bad_vault.yml 2026-06-18 19:34:02.000000000 +0000 @@ -0,0 +1,10 @@ +$ANSIBLE_VAULT;1.1;AES256 +65616232393566313961313431386633366639386133383230656230333934656366363636393730 +3266313739663639636161373033333865303461343936370a356463393635623664316464353061 +63396639643331303231616336663562303764653733326532316139643036336436396565653531 +3836383032626262620a316666363164626537346663383333376330623339633762363932613537 +32663462613532646139633364363136656132346661373331363164356162313762343337393666 +38653864363938316534333438643761623264376535336233656630376430346366333262313532 +33366662663137306431623464303561383730336466613166386136656364306436343032343631 +37396234633230626263373435623731616664653939343630393935626461396230663734373861 +3634 diff -Nru ansible-core-2.19.4/test/integration/targets/win_app_control/setup.yml ansible-core-2.19.11/test/integration/targets/win_app_control/setup.yml --- ansible-core-2.19.4/test/integration/targets/win_app_control/setup.yml 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/integration/targets/win_app_control/setup.yml 2026-06-18 19:34:02.000000000 +0000 @@ -82,7 +82,7 @@ - name: install OpenAuthenticode shell: | if (-not (Get-Module -Name OpenAuthenticode -ListAvailable | Where-Object Version -ge '0.5.0')) { - $url = 'https://ansible-ci-files.s3.us-east-1.amazonaws.com/test/integration/targets/win_app_control/openauthenticode.0.6.1.nupkg' + $url = 'https://ci-files.testing.ansible.com/test/integration/targets/win_app_control/openauthenticode.0.6.1.nupkg' Invoke-WebRequest -Uri $url -OutFile '{{ local_tmp_dir }}/openauthenticode.0.6.1.nupkg' Register-PSResourceRepository -Name AnsibleTemp -Trusted -Uri '{{ local_tmp_dir }}' diff -Nru ansible-core-2.19.4/test/lib/ansible_test/_data/completion/docker.txt ansible-core-2.19.11/test/lib/ansible_test/_data/completion/docker.txt --- ansible-core-2.19.4/test/lib/ansible_test/_data/completion/docker.txt 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/lib/ansible_test/_data/completion/docker.txt 2026-06-18 19:34:02.000000000 +0000 @@ -1,7 +1,7 @@ base image=quay.io/ansible/base-test-container:8.2.0 python=3.13,3.8,3.9,3.10,3.11,3.12 default image=quay.io/ansible/default-test-container:11.6.0 python=3.13,3.8,3.9,3.10,3.11,3.12 context=collection default image=quay.io/ansible/ansible-core-test-container:11.6.0 python=3.13,3.8,3.9,3.10,3.11,3.12 context=ansible-core -alpine321 image=quay.io/ansible/alpine321-test-container:9.1.0 python=3.12 cgroup=none audit=none -fedora41 image=quay.io/ansible/fedora41-test-container:9.0.0 python=3.13 cgroup=v2-only +alpine321 image=quay.io/ansible/alpine321-test-container:9.1.0 python=3.12 cgroup=none audit=none alias=alpine +fedora41 image=quay.io/ansible/fedora41-test-container:9.0.0 python=3.13 cgroup=v2-only alias=fedora ubuntu2204 image=quay.io/ansible/ubuntu2204-test-container:9.0.0 python=3.10 -ubuntu2404 image=quay.io/ansible/ubuntu2404-test-container:9.0.0 python=3.12 +ubuntu2404 image=quay.io/ansible/ubuntu2404-test-container:9.0.0 python=3.12 alias=ubuntu diff -Nru ansible-core-2.19.4/test/lib/ansible_test/_data/completion/remote.txt ansible-core-2.19.11/test/lib/ansible_test/_data/completion/remote.txt --- ansible-core-2.19.4/test/lib/ansible_test/_data/completion/remote.txt 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/lib/ansible_test/_data/completion/remote.txt 2026-06-18 19:34:02.000000000 +0000 @@ -1,15 +1,15 @@ -alpine/3.21 python=3.12 become=doas_sudo provider=aws arch=x86_64 +alpine/3.21 python=3.12 become=doas_sudo provider=aws arch=x86_64 alias=alpine/3,alpine/latest alpine become=doas_sudo provider=aws arch=x86_64 -fedora/41 python=3.13 become=sudo provider=aws arch=x86_64 +fedora/41 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 -freebsd/14.2 python=3.11 python_dir=/usr/local/bin become=su_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 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=parallels arch=x86_64 -macos python_dir=/usr/local/bin become=sudo provider=parallels arch=x86_64 -rhel/9.5 python=3.9,3.12 become=sudo provider=aws arch=x86_64 -rhel/10.0 python=3.12 become=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 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 +ubuntu/24.04 python=3.12 become=sudo provider=aws arch=x86_64 alias=ubuntu/latest ubuntu become=sudo provider=aws arch=x86_64 diff -Nru ansible-core-2.19.4/test/lib/ansible_test/_data/completion/windows.txt ansible-core-2.19.11/test/lib/ansible_test/_data/completion/windows.txt --- ansible-core-2.19.4/test/lib/ansible_test/_data/completion/windows.txt 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/lib/ansible_test/_data/completion/windows.txt 2026-06-18 19:34:02.000000000 +0000 @@ -1,5 +1,5 @@ windows/2016 provider=aws arch=x86_64 connection=winrm+http windows/2019 provider=aws arch=x86_64 connection=winrm+https windows/2022 provider=aws arch=x86_64 connection=winrm+https -windows/2025 provider=aws arch=x86_64 connection=psrp+http +windows/2025 provider=aws arch=x86_64 connection=psrp+http alias=windows/latest windows provider=aws arch=x86_64 connection=winrm+https diff -Nru ansible-core-2.19.4/test/lib/ansible_test/_internal/ansible_util.py ansible-core-2.19.11/test/lib/ansible_test/_internal/ansible_util.py --- ansible-core-2.19.4/test/lib/ansible_test/_internal/ansible_util.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/lib/ansible_test/_internal/ansible_util.py 2026-06-18 19:34:02.000000000 +0000 @@ -4,6 +4,7 @@ import json import os +import pathlib import shutil import typing as t @@ -12,6 +13,10 @@ SOFT_RLIMIT_NOFILE, ) +from .io import ( + write_text_file, +) + from .util import ( common_environment, ApplicationError, @@ -22,6 +27,7 @@ ANSIBLE_SOURCE_ROOT, ANSIBLE_TEST_TOOLS_ROOT, MODE_FILE_EXECUTE, + get_ansible_version, raw_command, verified_chmod, ) @@ -248,15 +254,12 @@ raise RuntimeError(path) -# noinspection PyUnusedLocal @mutex def get_ansible_python_path(args: CommonConfig) -> str: """ Return a directory usable for PYTHONPATH, containing only the ansible package. If a temporary directory is required, it will be cached for the lifetime of the process and cleaned up at exit. """ - del args # not currently used - try: return get_ansible_python_path.python_path # type: ignore[attr-defined] except AttributeError: @@ -273,11 +276,38 @@ os.symlink(ANSIBLE_LIB_ROOT, os.path.join(python_path, 'ansible')) + if not args.explain: + generate_dist_info(python_path) + get_ansible_python_path.python_path = python_path # type: ignore[attr-defined] return python_path +def generate_dist_info(path: str) -> None: + """Generate a dist-info in the specified base directory.""" + version = get_ansible_version() + metadata = f'''\ +Metadata-Version: 2.1 +Name: ansible-core +Version: {version} +''' + python_path = pathlib.Path(path) + + current_dist_info = python_path / f'ansible_core-{version}.dist-info' + + for dist_info in pathlib.Path(path).glob('ansible_core-*.dist-info'): + if dist_info == current_dist_info: + continue + shutil.rmtree(dist_info, ignore_errors=True) + + metadata_path = current_dist_info / 'METADATA' + if metadata_path.is_file(): + return + + write_text_file(str(metadata_path), metadata, create_directories=True) + + class CollectionDetail: """Collection detail.""" diff -Nru ansible-core-2.19.4/test/lib/ansible_test/_internal/ci/azp.py ansible-core-2.19.11/test/lib/ansible_test/_internal/ci/azp.py --- ansible-core-2.19.4/test/lib/ansible_test/_internal/ci/azp.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/lib/ansible_test/_internal/ci/azp.py 2026-06-18 19:34:02.000000000 +0000 @@ -2,6 +2,7 @@ from __future__ import annotations +import json import os import tempfile import uuid @@ -27,6 +28,7 @@ from ..util import ( display, + ApplicationError, MissingEnvironmentVariable, ) @@ -220,6 +222,19 @@ self.diff = [] def get_successful_merge_run_commits(self) -> set[str]: + """ + Return a set of recent successful merge commits from Azure Pipelines. + A warning will be displayed and no commits returned if an error occurs. + """ + try: + commits = self._get_successful_merge_run_commits() + except ApplicationError as ex: + commits = set() + display.warning(f'Cannot determine changes. All tests will be executed. Reason: {ex}') + + return commits + + def _get_successful_merge_run_commits(self) -> set[str]: """Return a set of recent successful merge commits from Azure Pipelines.""" parameters = dict( maxBuildsPerDefinition=100, # max 5000 @@ -230,20 +245,29 @@ repositoryId='%s/%s' % (self.org, self.project), ) - url = '%s%s/_apis/build/builds?api-version=6.0&%s' % (self.org_uri, self.project, urllib.parse.urlencode(parameters)) + url = '%s%s/_apis/build/builds?api-version=7.1&%s' % (self.org_uri, self.project, urllib.parse.urlencode(parameters)) http = HttpClient(self.args, always=True) response = http.get(url) - # noinspection PyBroadException try: - result = response.json() - except Exception: # pylint: disable=broad-except - # most likely due to a private project, which returns an HTTP 203 response with HTML - display.warning('Unable to find project. Cannot determine changes. All tests will be executed.') - return set() + result = json.loads(response.response) + result_type = 'JSON' + except json.JSONDecodeError: + result = ... + result_type = 'Non-JSON' - commits = set(build['sourceVersion'] for build in result['value']) + result_description = f'HTTP {response.status_code} {result_type} result' + + if response.status_code != 200 or result is ...: + raise ApplicationError(f'Unable to find project due to {result_description}.') + + try: + commits = {build['sourceVersion'] for build in result['value']} + except KeyError as ex: + raise ApplicationError(f'Missing {ex.args[0]!r} key in response from {result_description}.') from ex + except (ValueError, TypeError) as ex: + raise ApplicationError(f'Unexpected response format from {result_description}: {ex}') from ex return commits diff -Nru ansible-core-2.19.4/test/lib/ansible_test/_internal/ci/gha.py ansible-core-2.19.11/test/lib/ansible_test/_internal/ci/gha.py --- ansible-core-2.19.4/test/lib/ansible_test/_internal/ci/gha.py 1970-01-01 00:00:00.000000000 +0000 +++ ansible-core-2.19.11/test/lib/ansible_test/_internal/ci/gha.py 2026-06-18 19:34:02.000000000 +0000 @@ -0,0 +1,106 @@ +"""Support code for working with GitHub Actions.""" + +from __future__ import annotations + +import os +import typing as t + +from ..config import ( + CommonConfig, + TestConfig, +) + +from ..util import ( + ApplicationError, + MissingEnvironmentVariable, +) + +from . import ( + AuthContext, + CIProvider, + GeneratingAuthHelper, +) + +CODE = 'gha' +JOB_ID_ENV_VAR = 'ANSIBLE_TEST_GHA_JOB_ID' +ARTIFACT_ID_ENV_VAR = 'ANSIBLE_TEST_GHA_SSH_KEY_ARTIFACT_ID' + + +class GitHubActions(CIProvider): + """CI provider implementation for GitHub Actions.""" + + def __init__(self) -> None: + self.auth = GitHubActionsAuthHelper() + + @staticmethod + def is_supported() -> bool: + """Return True if this provider is supported in the current running environment.""" + return JOB_ID_ENV_VAR in os.environ and ARTIFACT_ID_ENV_VAR in os.environ + + @property + def code(self) -> str: + """Return a unique code representing this provider.""" + return CODE + + @property + def name(self) -> str: + """Return descriptive name for this provider.""" + return 'GitHub Actions' + + def generate_resource_prefix(self) -> str: + """Return a resource prefix specific to this CI provider.""" + keys = [ + 'GITHUB_REPOSITORY', + JOB_ID_ENV_VAR, + ] + + try: + segments = [os.environ[key] for key in keys] + except KeyError as ex: + raise MissingEnvironmentVariable(name=ex.args[0]) from None + + prefix = '-'.join(['gha'] + segments) + + return prefix + + def get_base_commit(self, args: CommonConfig) -> str: + """Return the base commit or an empty string.""" + return '' + + def detect_changes(self, args: TestConfig) -> t.Optional[list[str]]: + """Initialize change detection.""" + return None + + def supports_core_ci_auth(self) -> bool: + """Return True if Ansible Core CI is supported.""" + return True + + def prepare_core_ci_request(self, config: dict[str, object], context: AuthContext) -> dict[str, object]: + try: + owner, name = os.environ['GITHUB_REPOSITORY'].split('/', 1) + + request: dict[str, object] = dict( + type="gha:ssh", + config=config, + repository_owner=owner, + repository_name=name, + job_id=int(os.environ[JOB_ID_ENV_VAR]), + artifact_id=int(os.environ[ARTIFACT_ID_ENV_VAR]), + ) + except KeyError as ex: + raise MissingEnvironmentVariable(name=ex.args[0]) from None + + self.auth.sign_request(request, context) + + return request + + def get_git_details(self, args: CommonConfig) -> t.Optional[dict[str, t.Any]]: + """Return details about git in the current environment.""" + return None + + +class GitHubActionsAuthHelper(GeneratingAuthHelper): + """Authentication helper for GitHub Actions.""" + + def generate_key_pair(self) -> None: + raise ApplicationError(f'Missing SSH private key: {self.private_key_file}') diff -Nru ansible-core-2.19.4/test/lib/ansible_test/_internal/cli/compat.py ansible-core-2.19.11/test/lib/ansible_test/_internal/cli/compat.py --- ansible-core-2.19.4/test/lib/ansible_test/_internal/cli/compat.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/lib/ansible_test/_internal/cli/compat.py 2026-06-18 19:34:02.000000000 +0000 @@ -20,7 +20,6 @@ display, filter_args, sorted_versions, - str_to_version, ) from ..docker_util import ( @@ -31,6 +30,7 @@ docker_completion, remote_completion, filter_completion, + windows_completion, ) from ..host_configs import ( @@ -69,9 +69,7 @@ def get_fallback_remote_controller() -> str: """Return the remote fallback platform for the controller.""" - platform = 'freebsd' # lower cost than RHEL and macOS - candidates = [item for item in filter_completion(remote_completion()).values() if item.controller_supported and item.platform == platform] - fallback = sorted(candidates, key=lambda value: str_to_version(value.version), reverse=True)[0] + fallback = [item for name, item in filter_completion(remote_completion()).items() if item.controller_supported and name == "freebsd/latest"][0] return fallback.name @@ -351,17 +349,17 @@ if docker_config.controller_supported: if controller_python(options.python) or not options.python: - controller = DockerConfig(name=options.docker, python=native_python(options), + controller = DockerConfig(name=docker_config.name, python=native_python(options), privileged=options.docker_privileged, seccomp=options.docker_seccomp, memory=options.docker_memory) targets = controller_targets(mode, options, controller) else: controller_fallback = f'docker:{options.docker}', f'--docker {options.docker} --python {options.python}', FallbackReason.PYTHON - controller = DockerConfig(name=options.docker) + controller = DockerConfig(name=docker_config.name) targets = controller_targets(mode, options, controller) else: controller_fallback = f'docker:{docker_fallback}', f'--docker {options.docker}', FallbackReason.ENVIRONMENT controller = DockerConfig(name=docker_fallback) - targets = [DockerConfig(name=options.docker, python=native_python(options), + targets = [DockerConfig(name=docker_config.name, python=native_python(options), privileged=options.docker_privileged, seccomp=options.docker_seccomp, memory=options.docker_memory)] else: if not options.python: @@ -386,23 +384,25 @@ if remote_config.controller_supported: if controller_python(options.python) or not options.python: - controller = PosixRemoteConfig(name=options.remote, python=native_python(options), provider=options.remote_provider, + controller = PosixRemoteConfig(name=remote_config.name, python=native_python(options), provider=options.remote_provider, arch=options.remote_arch) targets = controller_targets(mode, options, controller) else: controller_fallback = f'remote:{options.remote}', f'--remote {options.remote} --python {options.python}', FallbackReason.PYTHON - controller = PosixRemoteConfig(name=options.remote, provider=options.remote_provider, arch=options.remote_arch) + controller = PosixRemoteConfig(name=remote_config.name, provider=options.remote_provider, arch=options.remote_arch) targets = controller_targets(mode, options, controller) else: context, reason = f'--remote {options.remote}', FallbackReason.ENVIRONMENT controller = None - targets = [PosixRemoteConfig(name=options.remote, python=native_python(options), provider=options.remote_provider, arch=options.remote_arch)] + targets = [PosixRemoteConfig(name=remote_config.name, python=native_python(options), provider=options.remote_provider, + arch=options.remote_arch)] elif mode == TargetMode.SHELL and options.remote.startswith('windows/'): if options.python and options.python not in CONTROLLER_PYTHON_VERSIONS: raise ControllerNotSupportedError(f'--python {options.python}') + name = resolve_windows_names([options.remote.removeprefix("windows/")])[0] controller = OriginConfig(python=native_python(options)) - targets = [WindowsRemoteConfig(name=options.remote, provider=options.remote_provider, arch=options.remote_arch)] + targets = [WindowsRemoteConfig(name=name, provider=options.remote_provider, arch=options.remote_arch)] else: if not options.python: raise PythonVersionUnspecifiedError(f'--remote {options.remote}') @@ -471,8 +471,8 @@ """Return a list of non-POSIX targets if the target mode is non-POSIX.""" if mode == TargetMode.WINDOWS_INTEGRATION: if options.windows: - targets = [WindowsRemoteConfig(name=f'windows/{version}', provider=options.remote_provider, arch=options.remote_arch) - for version in options.windows] + names = resolve_windows_names(options.windows) + targets = [WindowsRemoteConfig(name=name, provider=options.remote_provider, arch=options.remote_arch) for name in names] else: targets = [WindowsInventoryConfig(path=options.inventory)] elif mode == TargetMode.NETWORK_INTEGRATION: @@ -496,6 +496,16 @@ return targets +def resolve_windows_names(versions: list[str]) -> list[str]: + """Resolve a list of Windows versions into version names, resolving any aliases.""" + windows_completions = filter_completion(windows_completion()) + + names = [f'windows/{version}' for version in versions] # map versions to names + names = [windows_completions[name].name if name in windows_completions else name for name in names] # resolve aliases + + return names + + def default_targets( mode: TargetMode, controller: ControllerHostConfig, diff -Nru ansible-core-2.19.4/test/lib/ansible_test/_internal/cli/environments.py ansible-core-2.19.11/test/lib/ansible_test/_internal/cli/environments.py --- ansible-core-2.19.4/test/lib/ansible_test/_internal/cli/environments.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/lib/ansible_test/_internal/cli/environments.py 2026-06-18 19:34:02.000000000 +0000 @@ -640,10 +640,10 @@ def get_windows_platform_choices() -> list[str]: - """Return a list of supported Windows versions matching the given prefix.""" - return sorted(f'windows/{windows.version}' for windows in filter_completion(windows_completion()).values()) + """Return a list of supported Windows version names.""" + return sorted(filter_completion(windows_completion())) def get_windows_version_choices() -> list[str]: """Return a list of supported Windows versions.""" - return sorted(windows.version for windows in filter_completion(windows_completion()).values()) + return sorted(name.removeprefix("windows/") for name in get_windows_platform_choices()) diff -Nru ansible-core-2.19.4/test/lib/ansible_test/_internal/commands/integration/__init__.py ansible-core-2.19.11/test/lib/ansible_test/_internal/commands/integration/__init__.py --- ansible-core-2.19.4/test/lib/ansible_test/_internal/commands/integration/__init__.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/lib/ansible_test/_internal/commands/integration/__init__.py 2026-06-18 19:34:02.000000000 +0000 @@ -61,6 +61,7 @@ from ...util import ( ApplicationError, display, + get_ansible_version, SubprocessError, remove_tree, ) @@ -808,6 +809,7 @@ ANSIBLE_CALLBACKS_ENABLED=','.join(sorted(set(callback_plugins))), ANSIBLE_TEST_CI=args.metadata.ci_provider or get_ci_provider().code, ANSIBLE_TEST_COVERAGE='check' if args.coverage_check else ('yes' if args.coverage else ''), + ANSIBLE_TEST_ANSIBLE_VERSION=get_ansible_version(), OUTPUT_DIR=test_dir, INVENTORY_PATH=os.path.abspath(inventory_path), ) diff -Nru ansible-core-2.19.4/test/lib/ansible_test/_internal/completion.py ansible-core-2.19.11/test/lib/ansible_test/_internal/completion.py --- ansible-core-2.19.4/test/lib/ansible_test/_internal/completion.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/lib/ansible_test/_internal/completion.py 2026-06-18 19:34:02.000000000 +0000 @@ -6,6 +6,8 @@ import dataclasses import enum import os +import re +import sys import typing as t from .constants import ( @@ -17,6 +19,8 @@ ANSIBLE_TEST_DATA_ROOT, cache, read_lines_without_comments, + str_to_version, + InternalError, ) from .data import ( @@ -61,6 +65,11 @@ def is_default(self) -> bool: """True if the completion entry is only used for defaults, otherwise False.""" + @property + def sort_key(self) -> tuple[str, tuple[int, ...]]: + """Key used for sorting completion entries.""" + return '', (0,) + @dataclasses.dataclass(frozen=True) class PosixCompletionConfig(CompletionConfig, metaclass=abc.ABCMeta): @@ -114,6 +123,16 @@ arch: t.Optional[str] = None @property + def sort_key(self) -> tuple[str, tuple[int, ...]]: + """Key used for sorting completion entries.""" + try: + version = str_to_version(self.version) + except ValueError: + version = (sys.maxsize,) + + return self.platform, version + + @property def platform(self) -> str: """The name of the platform.""" return self.name.partition('/')[0] @@ -176,6 +195,19 @@ placeholder: bool = False @property + def sort_key(self) -> tuple[str, tuple[int, ...]]: + """Key used for sorting completion entries.""" + match = re.match('^(?P[a-z]+)(?P[0-9]*)$', self.name) + platform = match.group('platform') + + try: + version = str_to_version(match.group('version')) + except ValueError: + version = (sys.maxsize,) + + return platform, version + + @property def is_default(self) -> bool: """True if the completion entry is only used for defaults, otherwise False.""" return False @@ -263,12 +295,24 @@ context = 'ansible-core' items = {name: data for name, data in [parse_completion_entry(line) for line in lines] if data.get('context', context) == context} + aliases: dict[tuple[str, str], dict[str, str]] = {} + aliases_seen: set[str] = set() - for item in items.values(): + for item_name, item in items.items(): item.pop('context', None) item.pop('placeholder', None) + if alias := item.pop('alias', None): + for aliased_name in alias.split(','): + if aliased_name in aliases_seen: + raise InternalError(f"Duplicate alias {aliased_name!r} found for {name!r} completion.") + + aliases_seen.add(aliased_name) + aliases[(aliased_name, item_name)] = item + completion = {name: completion_type(name=name, **data) for name, data in items.items()} + completion |= {an[0]: completion_type(name=an[1], **data) for an, data in aliases.items()} + completion = dict(sorted(completion.items(), key=lambda entry: entry[1].sort_key)) return completion diff -Nru ansible-core-2.19.4/test/lib/ansible_test/_internal/constants.py ansible-core-2.19.11/test/lib/ansible_test/_internal/constants.py --- ansible-core-2.19.4/test/lib/ansible_test/_internal/constants.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/lib/ansible_test/_internal/constants.py 2026-06-18 19:34:02.000000000 +0000 @@ -23,7 +23,7 @@ 'default', 'aws', 'azure', - 'parallels', + 'mac', ] SECCOMP_CHOICES = [ diff -Nru ansible-core-2.19.4/test/lib/ansible_test/_internal/core_ci.py ansible-core-2.19.11/test/lib/ansible_test/_internal/core_ci.py --- ansible-core-2.19.4/test/lib/ansible_test/_internal/core_ci.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/lib/ansible_test/_internal/core_ci.py 2026-06-18 19:34:02.000000000 +0000 @@ -137,7 +137,7 @@ class AnsibleCoreCI: """Client for Ansible Core CI services.""" - DEFAULT_ENDPOINT = 'https://ansible-core-ci.testing.ansible.com' + DEFAULT_ENDPOINT = 'https://api.ci.core.ansible.com' def __init__( self, diff -Nru ansible-core-2.19.4/test/lib/ansible_test/_internal/host_configs.py ansible-core-2.19.11/test/lib/ansible_test/_internal/host_configs.py --- ansible-core-2.19.4/test/lib/ansible_test/_internal/host_configs.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/lib/ansible_test/_internal/host_configs.py 2026-06-18 19:34:02.000000000 +0000 @@ -384,6 +384,7 @@ super().apply_defaults(context, defaults) self.become = self.become or defaults.become + self.name = defaults.name @property def have_root(self) -> bool: @@ -413,6 +414,7 @@ super().apply_defaults(context, defaults) self.connection = self.connection or defaults.connection + self.name = defaults.name @dataclasses.dataclass diff -Nru ansible-core-2.19.4/test/lib/ansible_test/_util/controller/sanity/code-smell/runtime-metadata.py ansible-core-2.19.11/test/lib/ansible_test/_util/controller/sanity/code-smell/runtime-metadata.py --- ansible-core-2.19.4/test/lib/ansible_test/_util/controller/sanity/code-smell/runtime-metadata.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/lib/ansible_test/_util/controller/sanity/code-smell/runtime-metadata.py 2026-06-18 19:34:02.000000000 +0000 @@ -135,7 +135,7 @@ try: result = collection_detail.read_manifest_json('.') or collection_detail.read_galaxy_yml('.') version = SemanticVersion() - version.parse(result['version']) + version.parse(result['version'].split('-', 1)[0].split('+', 1)[0]) return version except Exception: # pylint: disable=broad-except # We do not care why it fails, in case we cannot get the version diff -Nru ansible-core-2.19.4/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/schema.py ansible-core-2.19.11/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/schema.py --- ansible-core-2.19.4/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/schema.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/schema.py 2026-06-18 19:34:02.000000000 +0000 @@ -795,11 +795,10 @@ Required('why'): doc_string, 'alternative': doc_string, 'alternatives': doc_string, + Required('removed_from_collection'): collection_name, + 'removed': Any(True), } - if for_collection: - main_fields.update({Required('removed_from_collection'): collection_name, 'removed': Any(True)}) - date_schema = { Required('removed_at_date'): date(), } diff -Nru ansible-core-2.19.4/test/lib/ansible_test/_util/target/setup/bootstrap.sh ansible-core-2.19.11/test/lib/ansible_test/_util/target/setup/bootstrap.sh --- ansible-core-2.19.4/test/lib/ansible_test/_util/target/setup/bootstrap.sh 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/lib/ansible_test/_util/target/setup/bootstrap.sh 2026-06-18 19:34:02.000000000 +0000 @@ -190,7 +190,7 @@ 13.5/3.11) # defaults available ;; - 14.2/3.11) + 14.3/3.11) # defaults available ;; *) @@ -231,7 +231,7 @@ # make additional wheels available for packages which lack them for this platform echo "# generated by ansible-test [global] -extra-index-url = https://spare-tire.testing.ansible.com/simple/ +extra-index-url = https://spare-tire.core.ansible.com/simple/ prefer-binary = yes " > /etc/pip.conf @@ -285,9 +285,13 @@ # Instead, ansible-test will install it using pip. # packaging and resolvelib are missing for controller supported Python versions, so we just # skip them and let ansible-test install them from PyPI. + # + # sqlite-libs needs to be specified currently to get sqlite3 imports working + # https://redhat.atlassian.net/browse/RHEL-178008 if [ "${controller}" ]; then packages=" ${packages} + sqlite-libs ${py_pkg_prefix}-cryptography ${py_pkg_prefix}-pyyaml " diff -Nru ansible-core-2.19.4/test/sanity/code-smell/no-s3.json ansible-core-2.19.11/test/sanity/code-smell/no-s3.json --- ansible-core-2.19.4/test/sanity/code-smell/no-s3.json 1970-01-01 00:00:00.000000000 +0000 +++ ansible-core-2.19.11/test/sanity/code-smell/no-s3.json 2026-06-18 19:34:02.000000000 +0000 @@ -0,0 +1,4 @@ +{ + "text": true, + "output": "path-line-column-message" +} diff -Nru ansible-core-2.19.4/test/sanity/code-smell/no-s3.py ansible-core-2.19.11/test/sanity/code-smell/no-s3.py --- ansible-core-2.19.4/test/sanity/code-smell/no-s3.py 1970-01-01 00:00:00.000000000 +0000 +++ ansible-core-2.19.11/test/sanity/code-smell/no-s3.py 2026-06-18 19:34:02.000000000 +0000 @@ -0,0 +1,27 @@ +""" +Disallow direct linking to S3 buckets. +S3 buckets should be accessed through a CloudFront distribution. +""" + +from __future__ import annotations + +import re +import sys + + +def main(): + """Main entry point.""" + for path in sys.argv[1:] or sys.stdin.read().splitlines(): + with open(path, 'rb') as path_fd: + for line, b_text in enumerate(path_fd.readlines()): + try: + text = b_text.decode() + except UnicodeDecodeError: + continue + + if match := re.search(r'(http.*?s3\..*?amazonaws\.com)', text): + print(f'{path}:{line + 1}:{match.start(1) + 1}: use a CloudFront distribution instead of an S3 bucket: {match.group(1)}') + + +if __name__ == '__main__': + main() diff -Nru ansible-core-2.19.4/test/units/ansible_test/_internal/ci/test_azp.py ansible-core-2.19.11/test/units/ansible_test/_internal/ci/test_azp.py --- ansible-core-2.19.4/test/units/ansible_test/_internal/ci/test_azp.py 1970-01-01 00:00:00.000000000 +0000 +++ ansible-core-2.19.11/test/units/ansible_test/_internal/ci/test_azp.py 2026-06-18 19:34:02.000000000 +0000 @@ -0,0 +1,96 @@ +from __future__ import annotations + +import argparse +import json +import os +import typing as t + +import pytest +import pytest_mock + +if t.TYPE_CHECKING: + from ansible_test._internal.ci.azp import AzurePipelinesChanges + + +def create_azure_pipelines_changes(mocker: pytest_mock.MockerFixture) -> AzurePipelinesChanges: + """Prepare an AzurePipelinesChanges instance for testing.""" + from ansible_test._internal.ci.azp import AzurePipelinesChanges + from ansible_test._internal.config import CommonConfig + + namespace = argparse.Namespace() + namespace.color = False + namespace.explain = False + namespace.verbosity = False + namespace.debug = False + namespace.truncate = False + namespace.redact = False + namespace.display_traceback = False + + config = CommonConfig(namespace, 'sanity') + + env = dict( + HOME=os.environ['HOME'], + SYSTEM_COLLECTIONURI='https://dev.azure.com/ansible/', + SYSTEM_TEAMPROJECT='ansible', + BUILD_REPOSITORY_PROVIDER='GitHub', + BUILD_SOURCEBRANCH='devel', + BUILD_SOURCEBRANCHNAME='devel', + ) + + mocker.patch.dict(os.environ, env, clear=True) + + return AzurePipelinesChanges(config) + + +@pytest.mark.parametrize("status_code,response,expected_commits,expected_warning", ( + # valid 200 responses + (200, dict(value=[]), None, None), + (200, dict(value=[dict(sourceVersion='abc')]), {'abc'}, None), + # invalid 200 responses + (200, 'not-json', None, "Unable to find project due to HTTP 200 Non-JSON result."), + (200, '"not-a-dict"', None, "Unexpected response format from HTTP 200 JSON result: string indices must be integers, not 'str'"), + (200, dict(value='not-a-list'), None, "Unexpected response format from HTTP 200 JSON result: string indices must be integers, not 'str'"), + (200, dict(value=['not-a-dict']), None, "Unexpected response format from HTTP 200 JSON result: string indices must be integers, not 'str'"), + (200, dict(), None, "Missing 'value' key in response from HTTP 200 JSON result."), + (200, dict(value=[{}]), None, "Missing 'sourceVersion' key in response from HTTP 200 JSON result."), + # non-200 responses + (404, '', None, "Unable to find project due to HTTP 404 Non-JSON result."), + (404, '""', None, "Unable to find project due to HTTP 404 JSON result."), + (404, dict(value=[]), None, "Unable to find project due to HTTP 404 JSON result."), +)) +def test_get_successful_merge_run_commits( + status_code: int, + response: object, + expected_commits: set[str] | None, + expected_warning: str | None, + mocker: pytest_mock.MockerFixture, +) -> None: + """Verify AZP commit retrieval handles invalid responses gracefully.""" + from ansible_test._internal.ci.azp import AzurePipelinesChanges + from ansible_test._internal.git import Git + from ansible_test._internal.http import HttpClient, HttpResponse + from ansible_test._internal.util import display + + if not isinstance(response, str): + response = json.dumps(response) + + if expected_warning: + expected_warning = f'Cannot determine changes. All tests will be executed. Reason: {expected_warning}' + + patched_get = mocker.patch.object(HttpClient, 'get', return_value=HttpResponse('GET', 'URL', status_code, response)) + patched_warning = mocker.patch.object(display, 'warning') + + mocker.patch.object(Git, 'run_git', return_value='') # avoid git + + spy_get_successful_merge_run_commits = mocker.spy(AzurePipelinesChanges, 'get_successful_merge_run_commits') + + create_azure_pipelines_changes(mocker) + + assert patched_get.call_count == 1 + + if expected_warning: + patched_warning.assert_called_once_with(expected_warning) + else: + patched_warning.assert_not_called() + + assert spy_get_successful_merge_run_commits.spy_return == (expected_commits or set()) diff -Nru ansible-core-2.19.4/test/units/module_utils/basic/test_human_to_bytes.py ansible-core-2.19.11/test/units/module_utils/basic/test_human_to_bytes.py --- ansible-core-2.19.4/test/units/module_utils/basic/test_human_to_bytes.py 1970-01-01 00:00:00.000000000 +0000 +++ ansible-core-2.19.11/test/units/module_utils/basic/test_human_to_bytes.py 2026-06-18 19:34:02.000000000 +0000 @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2025 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.module_utils.basic import AnsibleModule + + +@pytest.mark.parametrize('value, isbits, expected', [ + ("4KB", False, 4096), + ("4KB", None, 4096), + ("4Kb", True, 4096), +]) +def test_validator_function(value: str, isbits: bool | None, expected: int) -> None: + assert AnsibleModule.human_to_bytes(value, isbits=isbits) == expected + + +@pytest.mark.parametrize('value, expected', [ + ("4KB", 4096), +]) +def test_validator_function_default_isbits(value: str, expected: int) -> None: + assert AnsibleModule.human_to_bytes(value) == expected + + +@pytest.mark.parametrize('value, isbits', [ + ("4Kb", False), + ("4KB", True), +]) +def test_validator_functions(value: str, isbits: bool) -> None: + with pytest.raises(ValueError): + AnsibleModule.human_to_bytes(value, isbits=isbits) diff -Nru ansible-core-2.19.4/test/units/module_utils/basic/test_run_command.py ansible-core-2.19.11/test/units/module_utils/basic/test_run_command.py --- ansible-core-2.19.4/test/units/module_utils/basic/test_run_command.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/units/module_utils/basic/test_run_command.py 2026-06-18 19:34:02.000000000 +0000 @@ -255,3 +255,81 @@ assert subprocess_mock.Popen.call_args[1]['pass_fds'] == (101, 42) assert subprocess_mock.Popen.call_args[1]['close_fds'] is True + + +class TestRunCommandNoneRead: + """ + Test handling of read() returning None from non-blocking pipes. + + This tests the fix for issue #86920 where read() can return None + in certain edge cases with non-blocking I/O, which would cause + TypeError when trying to concatenate None to bytes. + """ + + class NoneReturningBytesIO(SpecialBytesIO): + """ + BytesIO that returns None on first read, then actual data. + + This simulates edge cases where non-blocking read() returns None + to indicate "no data available right now" rather than empty bytes. + """ + + def __init__(self, *args, **kwargs): + # Pop 'data' before calling super().__init__() since BytesIO doesn't accept it + self.data = kwargs.pop('data', b'test output') + self.read_count = 0 + super(TestRunCommandNoneRead.NoneReturningBytesIO, self).__init__(*args, **kwargs) + + def read(self, size=-1): + self.read_count += 1 + if self.read_count == 1: + # First read returns None (no data available) + return None + elif self.read_count == 2: + # Second read returns actual data + return self.data + else: + # Subsequent reads return empty bytes (EOF) + return b'' + + @pytest.mark.parametrize('stdin', [{}], indirect=['stdin']) + def test_none_from_stdout_read(self, mocker, rc_am): + """Test that None returned from stdout.read() doesn't cause TypeError.""" + rc_am._subprocess._output = { + mocker.sentinel.stdout: + self.NoneReturningBytesIO(fh=mocker.sentinel.stdout, data=b'command output'), + mocker.sentinel.stderr: + SpecialBytesIO(b'', fh=mocker.sentinel.stderr) + } + (rc, stdout, stderr) = rc_am.run_command('/bin/test') + assert rc == 0 + assert stdout == 'command output' + assert stderr == '' + + @pytest.mark.parametrize('stdin', [{}], indirect=['stdin']) + def test_none_from_stderr_read(self, mocker, rc_am): + """Test that None returned from stderr.read() doesn't cause TypeError.""" + rc_am._subprocess._output = { + mocker.sentinel.stdout: + SpecialBytesIO(b'', fh=mocker.sentinel.stdout), + mocker.sentinel.stderr: + self.NoneReturningBytesIO(fh=mocker.sentinel.stderr, data=b'error output') + } + (rc, stdout, stderr) = rc_am.run_command('/bin/test') + assert rc == 0 + assert stdout == '' + assert stderr == 'error output' + + @pytest.mark.parametrize('stdin', [{}], indirect=['stdin']) + def test_none_from_both_pipes(self, mocker, rc_am): + """Test that None returned from both pipes doesn't cause TypeError.""" + rc_am._subprocess._output = { + mocker.sentinel.stdout: + self.NoneReturningBytesIO(fh=mocker.sentinel.stdout, data=b'stdout data'), + mocker.sentinel.stderr: + self.NoneReturningBytesIO(fh=mocker.sentinel.stderr, data=b'stderr data') + } + (rc, stdout, stderr) = rc_am.run_command('/bin/test') + assert rc == 0 + assert stdout == 'stdout data' + assert stderr == 'stderr data' diff -Nru ansible-core-2.19.4/test/units/modules/test_get_url.py ansible-core-2.19.11/test/units/modules/test_get_url.py --- ansible-core-2.19.4/test/units/modules/test_get_url.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/units/modules/test_get_url.py 2026-06-18 19:34:02.000000000 +0000 @@ -8,22 +8,59 @@ from ansible.modules.get_url import parse_digest_lines +FILENAME = "sample.txt" + @pytest.mark.parametrize( ("lines", "expected"), [ pytest.param( [ + "2a32d433bf82355a3f78318a5affa21866c9a98b151785494b386e6b08f40b25 sample.txt", + ], + [("2a32d433bf82355a3f78318a5affa21866c9a98b151785494b386e6b08f40b25", FILENAME)], + id="single-line-digest-single-space", + ), + pytest.param( + [ + "2a32d433bf82355a3f78318a5affa21866c9a98b151785494b386e6b08f40b25 sample.txt", + ], + [("2a32d433bf82355a3f78318a5affa21866c9a98b151785494b386e6b08f40b25", FILENAME)], + id="single-line-digest-multiple-spaces", + ), + pytest.param( + [ + "2a32d433bf82355a3f78318a5affa21866c9a98b151785494b386e6b08f40b25 .sample.txt", + ], + [("2a32d433bf82355a3f78318a5affa21866c9a98b151785494b386e6b08f40b25", FILENAME)], + id="single-line-digest-multiple-spaces-with-dot", + ), + pytest.param( + [ + "2a32d433bf82355a3f78318a5affa21866c9a98b151785494b386e6b08f40b25 *sample.txt", + ], + [("2a32d433bf82355a3f78318a5affa21866c9a98b151785494b386e6b08f40b25", FILENAME)], + id="single-line-digest-multiple-spaces-with-asterisk", + ), + pytest.param( + [ + "2a32d433bf82355a3f78318a5affa21866c9a98b151785494b386e6b08f40b25 ./sample.txt", + ], + [("2a32d433bf82355a3f78318a5affa21866c9a98b151785494b386e6b08f40b25", FILENAME)], + id="single-line-digest-multiple-spaces-with-dot-and-slash", + ), + pytest.param( + [ "a97e6837f60cec6da4491bab387296bbcd72bdba", ], - [("a97e6837f60cec6da4491bab387296bbcd72bdba", "sample.txt")], + [("a97e6837f60cec6da4491bab387296bbcd72bdba", FILENAME)], id="single-line-digest", ), pytest.param( [ "a97e6837f60cec6da4491bab387296bbcd72bdba sample.txt", ], - [("a97e6837f60cec6da4491bab387296bbcd72bdba", "sample.txt")], + [("a97e6837f60cec6da4491bab387296bbcd72bdba", FILENAME)], id="GNU-style-digest", ), pytest.param( @@ -33,7 +70,7 @@ [ ( "b1b6ce5073c8fac263a8fc5edfffdbd5dec1980c784e09c5bc69f8fb6056f006.", - "sample.txt", + FILENAME, ) ], id="BSD-style-digest", @@ -41,5 +78,4 @@ ], ) def test_parse_digest_lines(lines, expected): - filename = "sample.txt" - assert parse_digest_lines(filename, lines) == expected + assert parse_digest_lines(filename=FILENAME, lines=lines) == expected diff -Nru ansible-core-2.19.4/test/units/playbook/test_task.py ansible-core-2.19.11/test/units/playbook/test_task.py --- ansible-core-2.19.4/test/units/playbook/test_task.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/units/playbook/test_task.py 2026-06-18 19:34:02.000000000 +0000 @@ -59,16 +59,16 @@ self.assertEqual(t.get_validated_value('delay', t.fattributes.get('delay'), delay, None), expected) bad_params = [ - ('E', AnsibleError), - ('1.E', AnsibleError), - ('E.1', AnsibleError), + 'E', + '1.E', + 'E.1', ] - for delay, expected in bad_params: - with self.subTest(f'type "{type(delay)} was cast to float w/o error', delay=delay, expected=expected): + for delay in bad_params: + with self.subTest(f'type "{type(delay)} was cast to float w/o error', delay=delay): p = dict(delay=delay) p.update(task_base) t = Task().load_data(p) - with self.assertRaises(expected): + with self.assertRaises(AnsibleError): dummy = t.get_validated_value('delay', t.fattributes.get('delay'), delay, None) def test_task_auto_name_with_role(self): diff -Nru ansible-core-2.19.4/test/units/plugins/become/test_su.py ansible-core-2.19.11/test/units/plugins/become/test_su.py --- ansible-core-2.19.4/test/units/plugins/become/test_su.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/units/plugins/become/test_su.py 2026-06-18 19:34:02.000000000 +0000 @@ -70,4 +70,4 @@ mocker.patch.object(become, 'get_option', return_value=['(invalid regex']) - assert become.check_password_prompt('(invalid regex:') is True + assert become.check_password_prompt(b'(invalid regex:') is True diff -Nru ansible-core-2.19.4/test/units/plugins/connection/test_winrm.py ansible-core-2.19.11/test/units/plugins/connection/test_winrm.py --- ansible-core-2.19.4/test/units/plugins/connection/test_winrm.py 2025-11-04 23:27:03.000000000 +0000 +++ ansible-core-2.19.11/test/units/plugins/connection/test_winrm.py 2026-06-18 19:34:02.000000000 +0000 @@ -63,7 +63,7 @@ '_winrm_kwargs': {'username': 'user@domain.com', 'password': None}, '_winrm_pass': None, - '_winrm_transport': ['kerberos', 'ssl'], + '_winrm_transport': ['kerberos'], '_winrm_user': 'user@domain.com' }, True @@ -78,7 +78,7 @@ '_winrm_kwargs': {'username': 'user@domain.com', 'password': None}, '_winrm_pass': None, - '_winrm_transport': ['ssl'], + '_winrm_transport': ['kerberos'], '_winrm_user': 'user@domain.com' }, False @@ -93,7 +93,7 @@ '_winrm_kwargs': {'username': 'user@domain.com', 'password': 'pass'}, '_winrm_pass': 'pass', - '_winrm_transport': ['kerberos', 'ssl'], + '_winrm_transport': ['kerberos'], '_winrm_user': 'user@domain.com' }, True diff -Nru ansible-core-2.19.4/test/units/utils/test_jsonrpc.py ansible-core-2.19.11/test/units/utils/test_jsonrpc.py --- ansible-core-2.19.4/test/units/utils/test_jsonrpc.py 1970-01-01 00:00:00.000000000 +0000 +++ ansible-core-2.19.11/test/units/utils/test_jsonrpc.py 2026-06-18 19:34:02.000000000 +0000 @@ -0,0 +1,31 @@ +from __future__ import annotations + +import json +import pickle + +from ansible._internal._datatag._tags import Origin +from ansible.utils.jsonrpc import JsonRpcServer + + +def test_response_type_cleansing() -> None: + """Avoid unpickling errors in module contexts by ensuring that non-scalar JsonRpc responses are not pickled with tags.""" + + class RPCTest: + def returns_list_with_tagged_str(self) -> list: + return [Origin(description="blar").tag("taggedstr")] + + s = JsonRpcServer() + s.register(RPCTest()) + req = dict(method="returns_list_with_tagged_str", id=1, params=(tuple(), {})) + jsonrpc_res = s.handle_request(json.dumps(req)) + + deserialized_res = json.loads(jsonrpc_res) + + pickled_res = deserialized_res.get("result") + + assert pickled_res is not None + + res = pickle.loads(pickled_res.encode(errors="surrogateescape")) + + assert res == ["taggedstr"] + assert not Origin.is_tagged_on(res[0])