Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package ansible-core-2.18 for 
openSUSE:Factory checked in at 2026-03-26 21:09:27
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/ansible-core-2.18 (Old)
 and      /work/SRC/openSUSE:Factory/.ansible-core-2.18.new.8177 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "ansible-core-2.18"

Thu Mar 26 21:09:27 2026 rev:6 rq:1342687 version:2.18.15

Changes:
--------
--- /work/SRC/openSUSE:Factory/ansible-core-2.18/ansible-core-2.18.changes      
2026-03-11 20:58:47.730523068 +0100
+++ 
/work/SRC/openSUSE:Factory/.ansible-core-2.18.new.8177/ansible-core-2.18.changes
    2026-03-27 06:36:02.852694177 +0100
@@ -1,0 +2,13 @@
+Wed Mar 25 13:11:00 UTC 2026 - Johannes Kastl 
<[email protected]>
+
+- update to 2.18.15:
+  * 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
+    - rpm_key - Use librpm library API instead of gpg utility to
+      support version 6 PGP keys (#86157).
+
+-------------------------------------------------------------------

Old:
----
  ansible_core-2.18.14.tar.gz
  ansible_core-2.18.14.tar.gz.sha256

New:
----
  ansible_core-2.18.15.tar.gz
  ansible_core-2.18.15.tar.gz.sha256

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ ansible-core-2.18.spec ++++++
--- /var/tmp/diff_new_pack.N8e4go/_old  2026-03-27 06:36:03.604725172 +0100
+++ /var/tmp/diff_new_pack.N8e4go/_new  2026-03-27 06:36:03.604725172 +0100
@@ -43,7 +43,7 @@
 %endif
 
 Name:           ansible-core-2.18
-Version:        2.18.14
+Version:        2.18.15
 Release:        0
 Summary:        Radically simple IT automation
 License:        GPL-3.0-or-later

++++++ ansible_core-2.18.14.tar.gz -> ansible_core-2.18.15.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ansible_core-2.18.14/PKG-INFO 
new/ansible_core-2.18.15/PKG-INFO
--- old/ansible_core-2.18.14/PKG-INFO   2026-02-23 23:49:04.000000000 +0100
+++ new/ansible_core-2.18.15/PKG-INFO   2026-03-23 18:38:33.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: ansible-core
-Version: 2.18.14
+Version: 2.18.15
 Summary: Radically simple IT automation
 Author: Ansible Project
 Project-URL: Homepage, https://ansible.com/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ansible_core-2.18.14/ansible_core.egg-info/PKG-INFO 
new/ansible_core-2.18.15/ansible_core.egg-info/PKG-INFO
--- old/ansible_core-2.18.14/ansible_core.egg-info/PKG-INFO     2026-02-23 
23:49:04.000000000 +0100
+++ new/ansible_core-2.18.15/ansible_core.egg-info/PKG-INFO     2026-03-23 
18:38:33.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: ansible-core
-Version: 2.18.14
+Version: 2.18.15
 Summary: Radically simple IT automation
 Author: Ansible Project
 Project-URL: Homepage, https://ansible.com/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/ansible_core-2.18.14/ansible_core.egg-info/SOURCES.txt 
new/ansible_core-2.18.15/ansible_core.egg-info/SOURCES.txt
--- old/ansible_core-2.18.14/ansible_core.egg-info/SOURCES.txt  2026-02-23 
23:49:04.000000000 +0100
+++ new/ansible_core-2.18.15/ansible_core.egg-info/SOURCES.txt  2026-03-23 
18:38:33.000000000 +0100
@@ -3371,6 +3371,7 @@
 test/integration/targets/roles_var_inheritance/roles/nested_dep/meta/main.yml
 test/integration/targets/roles_var_inheritance/roles/nested_dep/tasks/main.yml
 test/integration/targets/rpm_key/aliases
+test/integration/targets/rpm_key/files/TEST-V6-NON-PQC-KEY.cert
 test/integration/targets/rpm_key/meta/main.yml
 test/integration/targets/rpm_key/tasks/main.yaml
 test/integration/targets/rpm_key/tasks/rpm_key.yaml
@@ -4142,6 +4143,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
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ansible_core-2.18.14/changelogs/CHANGELOG-v2.18.rst 
new/ansible_core-2.18.15/changelogs/CHANGELOG-v2.18.rst
--- old/ansible_core-2.18.14/changelogs/CHANGELOG-v2.18.rst     2026-02-23 
23:49:04.000000000 +0100
+++ new/ansible_core-2.18.15/changelogs/CHANGELOG-v2.18.rst     2026-03-23 
18:38:33.000000000 +0100
@@ -4,6 +4,26 @@
 
 .. contents:: Topics
 
+v2.18.15
+========
+
+Release Summary
+---------------
+
+| Release Date: 2026-03-23
+| `Porting Guide 
<https://docs.ansible.com/ansible-core/2.18/porting_guides/porting_guide_core_2.18.html>`__
+
+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
+--------
+
+- rpm_key - Use librpm library API instead of gpg utility to support version 6 
PGP keys (https://github.com/ansible/ansible/issues/86157).
+
 v2.18.14
 ========
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ansible_core-2.18.14/changelogs/changelog.yaml 
new/ansible_core-2.18.15/changelogs/changelog.yaml
--- old/ansible_core-2.18.14/changelogs/changelog.yaml  2026-02-23 
23:49:04.000000000 +0100
+++ new/ansible_core-2.18.15/changelogs/changelog.yaml  2026-03-23 
18:38:33.000000000 +0100
@@ -773,6 +773,39 @@
     - ansible-test-api-endpoint.yml
     - ansible-test-spare-tire.yml
     release_date: '2026-02-17'
+  2.18.15:
+    changes:
+      release_summary: '| Release Date: 2026-03-23
+
+        | `Porting Guide 
<https://docs.ansible.com/ansible-core/2.18/porting_guides/porting_guide_core_2.18.html>`__
+
+        '
+    codename: Fool in the Rain
+    fragments:
+    - 2.18.15_summary.yaml
+    release_date: '2026-03-23'
+  2.18.15rc1:
+    changes:
+      bugfixes:
+      - rpm_key - Use librpm library API instead of gpg utility to support 
version
+        6 PGP keys (https://github.com/ansible/ansible/issues/86157).
+      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 
<https://docs.ansible.com/ansible-core/2.18/porting_guides/porting_guide_core_2.18.html>`__
+
+        '
+    codename: Fool in the Rain
+    fragments:
+    - 2.18.15rc1_summary.yaml
+    - 86237-rpm-key-librpm.yml
+    - ansible-test-completion-aliases.yml
+    - ansible-test-github-actions.yml
+    release_date: '2026-03-16'
   2.18.1rc1:
     changes:
       bugfixes:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/ansible_core-2.18.14/lib/ansible/module_utils/ansible_release.py 
new/ansible_core-2.18.15/lib/ansible/module_utils/ansible_release.py
--- old/ansible_core-2.18.14/lib/ansible/module_utils/ansible_release.py        
2026-02-23 23:49:04.000000000 +0100
+++ new/ansible_core-2.18.15/lib/ansible/module_utils/ansible_release.py        
2026-03-23 18:38:33.000000000 +0100
@@ -17,6 +17,6 @@
 
 from __future__ import annotations
 
-__version__ = '2.18.14'
+__version__ = '2.18.15'
 __author__ = 'Ansible, Inc.'
 __codename__ = "Fool in the Rain"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ansible_core-2.18.14/lib/ansible/modules/rpm_key.py 
new/ansible_core-2.18.15/lib/ansible/modules/rpm_key.py
--- old/ansible_core-2.18.14/lib/ansible/modules/rpm_key.py     2026-02-23 
23:49:04.000000000 +0100
+++ new/ansible_core-2.18.15/lib/ansible/modules/rpm_key.py     2026-03-23 
18:38:33.000000000 +0100
@@ -22,6 +22,7 @@
       description:
         - Key that will be modified. Can be a url, a file on the managed node, 
or a keyid if the key
           already exists in the database.
+        - This can also be the fingerprint when attempting to delete an 
already installed key.
       type: str
       required: true
     state:
@@ -85,17 +86,325 @@
 
 RETURN = r'''#'''
 
+import ctypes
+import ctypes.util
+import hashlib
 import re
 import os.path
 import tempfile
+import typing as _t
 
 # import module snippets
 from ansible.module_utils.basic import AnsibleModule
 from ansible.module_utils.urls import fetch_url
+from ansible.module_utils.compat.version import LooseVersion
 from ansible.module_utils.common.text.converters import to_native
 
+# Type alias for ctypes pointer to uint8 array (packet data)
+# Using Any here because ctypes._Pointer is private, but documenting the 
actual type
+PktPointer = _t.Any  # Actually: ctypes.POINTER(ctypes.c_uint8)
+
+
+class LibRPM:
+    """
+    Wrapper for librpm PGP key functions.
+
+    The APIs in librpm vary across different versions. Since this module must 
work on a variety of
+    systems, we are extremely limited in the API calls that we can guarantee 
will be available.
+    """
+
+    # Constants
+    PGPTAG_PUBLIC_KEY = 6
+    PGPTAG_PUBLIC_SUBKEY = 14
+
+    def __init__(self):
+        # Load the librpm library
+        if not (lib_path := ctypes.util.find_library('rpm')):
+            raise ImportError("Error: Could not find librpm library")
+
+        self._lib = ctypes.CDLL(lib_path)
+        self._libc = ctypes.CDLL(None)
+
+        # void free(void *ptr)
+        self._libc.free.argtypes = [ctypes.c_void_p]
+        self._libc.free.restype = None
+
+        # pgpArmor pgpParsePkts(const char *armor, uint8_t **pkt, size_t 
*pktlen)
+        self._lib.pgpParsePkts.argtypes = [
+            ctypes.c_char_p,
+            ctypes.POINTER(ctypes.POINTER(ctypes.c_uint8)),
+            ctypes.POINTER(ctypes.c_size_t)
+        ]
+        self._lib.pgpParsePkts.restype = ctypes.c_int
+
+        # Identify the version of the RPM library
+        _lib_rpmversion = ctypes.c_char_p.in_dll(self._lib, "RPMVERSION")
+        self._rpmversion = _lib_rpmversion.value.decode()
+
+    @property
+    def using_librpm6(self) -> bool:
+        """
+        Check if the librpm version in use is at least version 6.0.0.
+
+        RPM version 6.0.0 and higher uses fingerprints instead of short key ID 
everywhere. This changes
+        how we must approach certain operations, such as key deletion from the 
rpmdb.
+        """
+        if LooseVersion(self._rpmversion) >= LooseVersion('6.0.0'):
+            return True
+        return False
+
+    def _parse_armor(self, armor: str) -> tuple[PktPointer | None, int]:
+        """
+        Parse ASCII armored PGP data using pgpParsePkts().
+        Returns (pkt, pktlen) tuple or (None, 0) on error.
+        """
+        pkt = ctypes.POINTER(ctypes.c_uint8)()
+        pktlen = ctypes.c_size_t()
+
+        armor_bytes = armor.encode()
+        result = self._lib.pgpParsePkts(armor_bytes, ctypes.byref(pkt), 
ctypes.byref(pktlen))
+
+        if result < 0 or not pkt:
+            return None, 0
+
+        return pkt, pktlen.value
+
+    def _parse_packet_header(self, pkt: PktPointer, offset: int, pktlen: int) 
-> tuple[int | None, int, int]:
+        """
+        Parse a PGP packet header to get tag and packet length.
+        Returns (tag, body_length, header_length) or (None, 0, 0) on error.
+
+        Per RFC 9580 - Section 4.2: Packet Headers
+        https://www.rfc-editor.org/rfc/rfc9580.html#name-packet-headers
+        """
+        if offset >= pktlen:
+            return None, 0, 0
+
+        tag_byte = pkt[offset]
+
+        # Check if it's a new format packet (bit 6 set)
+        if tag_byte & 0x40:
+            # New format
+            tag = tag_byte & 0x3f  # bits 0-5 are packet type ID
+            offset += 1
+
+            if offset >= pktlen:
+                return None, 0, 0
+
+            first_len_byte = pkt[offset]
+
+            if first_len_byte < 192:
+                # One-octet length
+                return tag, first_len_byte, 2
+            elif first_len_byte < 224:
+                # Two-octet length
+                if offset + 1 >= pktlen:
+                    return None, 0, 0
+                length = ((first_len_byte - 192) << 8) + pkt[offset + 1] + 192
+                return tag, length, 3
+            elif first_len_byte == 255:
+                # Five-octet length
+                if offset + 4 >= pktlen:
+                    return None, 0, 0
+                length = (pkt[offset + 1] << 24) | (pkt[offset + 2] << 16) | \
+                         (pkt[offset + 3] << 8) | pkt[offset + 4]
+                return tag, length, 6
+            else:
+                # Partial body length (not supported here)
+                return None, 0, 0
+        else:
+            # Old format
+            tag = (tag_byte >> 2) & 0x0f
+            length_type = tag_byte & 0x03
+
+            if length_type == 0:
+                # One-octet length
+                if offset + 1 >= pktlen:
+                    return None, 0, 0
+                return tag, pkt[offset + 1], 2
+            elif length_type == 1:
+                # Two-octet length
+                if offset + 2 >= pktlen:
+                    return None, 0, 0
+                length = (pkt[offset + 1] << 8) | pkt[offset + 2]
+                return tag, length, 3
+            elif length_type == 2:
+                # Four-octet length
+                if offset + 4 >= pktlen:
+                    return None, 0, 0
+                length = (pkt[offset + 1] << 24) | (pkt[offset + 2] << 16) | \
+                         (pkt[offset + 3] << 8) | pkt[offset + 4]
+                return tag, length, 5
+            else:
+                # Indeterminate length (not supported)
+                return None, 0, 0
+
+    def _find_key_packets(self, pkt: PktPointer, pktlen: int) -> 
list[tuple[int, int]]:
+        """
+        Walk the packet stream and find all PGPTAG_PUBLIC_KEY and 
PGPTAG_PUBLIC_SUBKEY packets.
+        Returns list of (offset, total_packet_length) tuples.
+        """
+        key_packets: list[tuple[int, int]] = []
+        offset = 0
+
+        while offset < pktlen:
+            tag, body_len, header_len = self._parse_packet_header(pkt, offset, 
pktlen)
+
+            if tag is None:
+                break
+
+            if tag in (self.PGPTAG_PUBLIC_KEY, self.PGPTAG_PUBLIC_SUBKEY):
+                # Found a key packet
+                total_len = header_len + body_len
+                key_packets.append((offset, total_len))
+
+            # Move to next packet
+            offset += header_len + body_len
+
+        return key_packets
+
+    def _get_key_version(self, pkt: PktPointer, offset: int, pktlen: int) -> 
int | None:
+        """
+        Get the version byte from a key packet.
+        Returns version number (4 or 6) or None on error.
+        """
+        tag, dummy, header_len = self._parse_packet_header(pkt, offset, pktlen)
+        if tag is None:
+            return None
+
+        # Extract packet body (skip the packet header)
+        body_offset = offset + header_len
+        if body_offset >= pktlen:
+            return None
+
+        # First byte of body is the version
+        return pkt[body_offset]
+
+    def _compute_v4_fingerprint(self, pkt: PktPointer, offset: int, pktlen: 
int) -> str | None:
+        """
+        Compute V4 fingerprint from packet data.
+        For V4 keys, fingerprint = SHA-1(0x99 || 2-byte-length || packet_body)
+        Per RFC 4880 Section 12.2
+        """
+        tag, body_len, header_len = self._parse_packet_header(pkt, offset, 
pktlen)
+
+        if tag is None:
+            return None
+
+        # Extract packet body (skip the packet header)
+        body_offset = offset + header_len
+        if body_offset + body_len > pktlen:
+            return None
+
+        # Check if it's a V4 key (first byte of body should be 0x04)
+        if pkt[body_offset] != 0x04:
+            return None
+
+        # Build the data for fingerprint: 0x99 || 2-byte length || body
+        fp_data = bytearray()
+        fp_data.append(0x99)  # V4 public key packet tag
+        fp_data.append((body_len >> 8) & 0xFF)  # Length high byte
+        fp_data.append(body_len & 0xFF)  # Length low byte
+
+        # Append the packet body
+        for i in range(body_len):
+            fp_data.append(pkt[body_offset + i])
+
+        # Compute SHA-1 hash
+        return hashlib.sha1(fp_data).hexdigest().upper()
+
+    def _compute_v6_fingerprint(self, pkt: PktPointer, offset: int, pktlen: 
int) -> str | None:
+        """
+        Compute V6 fingerprint from packet data.
+        For V6 keys, fingerprint = SHA-256(0x9B || 4-byte-length || 
packet_body)
+        Per RFC 9580 Section 5.5.4
+        """
+        tag, body_len, header_len = self._parse_packet_header(pkt, offset, 
pktlen)
+
+        if tag is None:
+            return None
+
+        # Extract packet body (skip the packet header)
+        body_offset = offset + header_len
+        if body_offset + body_len > pktlen:
+            return None
+
+        # Check if it's a V6 key (first byte of body should be 0x06)
+        if pkt[body_offset] != 0x06:
+            return None
+
+        # Build the data for fingerprint: 0x9B || 4-byte length || body
+        fp_data = bytearray()
+        fp_data.append(0x9B)  # V6 public key packet tag
+        fp_data.append((body_len >> 24) & 0xFF)  # Length byte 1 (MSB)
+        fp_data.append((body_len >> 16) & 0xFF)  # Length byte 2
+        fp_data.append((body_len >> 8) & 0xFF)   # Length byte 3
+        fp_data.append(body_len & 0xFF)          # Length byte 4 (LSB)
+
+        # Append the packet body
+        for i in range(body_len):
+            fp_data.append(pkt[body_offset + i])
+
+        # Compute SHA-256 hash
+        return hashlib.sha256(fp_data).hexdigest().upper()
+
+    def identify_keys(self, armor: str) -> list[dict[str, str]]:
+        """Return a list of dicts with key ID (8-byte) and fingerprint for the 
primary key and each subkey"""
+        key_info: list[dict[str, str]] = []
+
+        pkt, pktlen = self._parse_armor(armor)
+        if not pkt:
+            raise Exception("Unable to parse PGP key armor")
+
+        # Find all key packets in the stream and compute their fingerprints.
+        key_packets = self._find_key_packets(pkt, pktlen)
+
+        for offset, dummy in key_packets:
+            # Detect key version
+            version = self._get_key_version(pkt, offset, pktlen)
+
+            if version == 0x04:
+                # V4 key
+                computed_fp = self._compute_v4_fingerprint(pkt, offset, pktlen)
+                if computed_fp:
+                    # V4: Key ID is the last 8 bytes (16 hex chars) of the 
fingerprint
+                    keyid_from_fp = computed_fp[-16:]
+                    key_info.append({'keyid': keyid_from_fp, 'fingerprint': 
computed_fp})
+            elif version == 0x06:
+                # V6 key
+                computed_fp = self._compute_v6_fingerprint(pkt, offset, pktlen)
+                if computed_fp:
+                    # V6: Key ID is the first 8 bytes (16 hex chars) of the 
fingerprint
+                    keyid_from_fp = computed_fp[:16]
+                    key_info.append({'keyid': keyid_from_fp, 'fingerprint': 
computed_fp})
+            else:
+                raise Exception(f"Unhandled key version {version:#04x}")
+
+        self._libc.free(pkt)
+
+        return key_info
 
-def is_pubkey(string):
+    def get_key_ids_from_armor(self, armor: str) -> list[str]:
+        """
+        Get the key IDs from the primary PGP key, and all subkeys of that key, 
from the ASCII armored key.
+
+        'armor' is expected to be a single ASCII armored PGP key (v4 or v6). 
The primary key should be the
+        first item in the results, followed by its subkeys. Returned key IDs 
are 8-byte (16 hex characters)
+        in length. This must be accounted for if comparing against the short 
key ID (4-bytes).
+        """
+        return [key['keyid'] for key in self.identify_keys(armor)]
+
+    def get_fingerprints_from_armor(self, armor: str) -> list[str]:
+        """
+        Get the fingerprints from the primary PGP key, and all subkeys of that 
key, from the ASCII armored key.
+
+        'armor' is expected to be a single ASCII armored PGP key (v4 or v6). 
The primary key should be the
+        first item in the results, followed by its subkeys.
+        """
+        return [key['fingerprint'] for key in self.identify_keys(armor)]
+
+
+def is_pubkey(string: str) -> bool:
     """Verifies if string is a pubkey"""
     pgp_regex = ".*?(-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP 
PUBLIC KEY BLOCK-----).*"
     return bool(re.match(pgp_regex, to_native(string, 
errors='surrogate_or_strict'), re.DOTALL))
@@ -103,13 +412,14 @@
 
 class RpmKey(object):
 
-    def __init__(self, module):
+    def __init__(self, module: AnsibleModule) -> None:
         # If the key is a url, we need to check if it's present to be 
idempotent,
         # to do that, we need to check the keyid, which we can get from the 
armor.
         keyfile = None
         should_cleanup_keyfile = False
         self.module = module
         self.rpm = self.module.get_bin_path('rpm', True)
+        self.rpmkeys = self.module.get_bin_path('rpmkeys', True)
         state = module.params['state']
         key = module.params['key']
         fingerprint = module.params['fingerprint']
@@ -120,9 +430,7 @@
                 fingerprint = [fingerprint]
             fingerprints = set(f.replace(' ', '').upper() for f in fingerprint)
 
-        self.gpg = self.module.get_bin_path('gpg')
-        if not self.gpg:
-            self.gpg = self.module.get_bin_path('gpg2', required=True)
+        self.librpm = LibRPM()
 
         if '://' in key:
             keyfile = self.fetch_key(key)
@@ -137,6 +445,8 @@
             self.module.fail_json(msg="Not a valid key %s" % key)
         keyid = self.normalize_keyid(keyid)
 
+        self.installed_keys = self.get_installed_keys()
+
         if state == 'present':
             if self.is_key_imported(keyid):
                 module.exit_json(changed=False)
@@ -161,7 +471,7 @@
             else:
                 module.exit_json(changed=False)
 
-    def fetch_key(self, url):
+    def fetch_key(self, url: str) -> str:
         """Downloads a key from url, returns a valid path to a gpg key"""
         rsp, info = fetch_url(self.module, url)
         if info['status'] != 200:
@@ -177,7 +487,7 @@
         tmpfile.close()
         return tmpname
 
-    def normalize_keyid(self, keyid):
+    def normalize_keyid(self, keyid: str) -> str:
         """Ensure a keyid doesn't have a leading 0x, has leading or trailing 
whitespace, and make sure is uppercase"""
         ret = keyid.strip().upper()
         if ret.startswith('0x'):
@@ -188,70 +498,133 @@
             return ret
 
     def getkeyid(self, keyfile):
-        stdout, stderr = self.execute_command([self.gpg, '--no-tty', 
'--batch', '--with-colons', '--fixed-list-mode', keyfile])
-        for line in stdout.splitlines():
-            line = line.strip()
-            if line.startswith('pub:'):
-                return line.split(':')[4]
-
-        self.module.fail_json(msg="Unexpected gpg output")
+        with open(keyfile, "r") as key_fd:
+            key_ids = self.librpm.get_key_ids_from_armor(key_fd.read())
+        if not key_ids:
+            self.module.fail_json(msg="Failed to get keyid")
+        return key_ids[0]
 
     def getfingerprints(self, keyfile):
-        stdout, stderr = self.execute_command([
-            self.gpg, '--no-tty', '--batch', '--with-colons',
-            '--fixed-list-mode', '--import', '--import-options', 'show-only',
-            '--dry-run', keyfile
-        ])
-
-        fingerprints = set()
-
-        for line in stdout.splitlines():
-            line = line.strip()
-            if line.startswith('fpr:'):
-                # As mentioned here,
-                #
-                # 
https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob_plain;f=doc/DETAILS
-                #
-                # The description of the `fpr` field says
-                #
-                # "fpr :: Fingerprint (fingerprint is in field 10)"
-                #
-                fingerprints.add(line.split(':')[9])
-
-        if fingerprints:
-            return fingerprints
-
-        self.module.fail_json(msg="Unexpected gpg output")
-
-    def is_keyid(self, keystr):
-        """Verifies if a key, as provided by the user is a keyid"""
+        with open(keyfile, "r") as key_fd:
+            fingerprints = 
self.librpm.get_fingerprints_from_armor(key_fd.read())
+        if not fingerprints:
+            self.module.fail_json(msg="Failed to get fingerprint")
+        return frozenset(fingerprints)
+
+    def is_keyid(self, keystr: str) -> re.Match[str] | None:
+        """
+        Verifies if a key, as provided by the user, is a key ID.
+
+        Note that this allows the short form of the key ID (4-bytes, or 8 hex 
characters), used in older
+        versions of RPM, while a full key ID is 8-bytes, or 16 hex characters.
+        """
+        keystr = keystr.replace(' ', '')
         return re.match('(0x)?[0-9a-f]{8}', keystr, flags=re.IGNORECASE)
 
-    def execute_command(self, cmd):
+    def execute_command(self, cmd: str | list[str]) -> tuple[str, str]:
         rc, stdout, stderr = self.module.run_command(cmd, 
use_unsafe_shell=True)
         if rc != 0:
             self.module.fail_json(msg=stderr)
         return stdout, stderr
 
-    def is_key_imported(self, keyid):
-        cmd = self.rpm + ' -q  gpg-pubkey'
+    def get_installed_keys(self) -> list[dict[str, str]]:
+        """
+        Get the key ID and fingerprint for every key installed on the system.
+
+        This will grab the armor string for every key reported from `rpm -q 
gpg-pubkey` and parse
+        it to obtain the key ID and fingerprint, including subkeys.
+        """
+        installed_keys = []
+
+        cmd = self.rpm + ' -q gpg-pubkey'
         rc, stdout, stderr = self.module.run_command(cmd)
         if rc != 0:  # No key is installed on system
-            return False
-        cmd += ' --qf "%{description}" | ' + self.gpg + ' --no-tty --batch 
--with-colons --fixed-list-mode -'
+            return []
+        cmd += ' --qf "%{description}"'
         stdout, stderr = self.execute_command(cmd)
+
+        # Split the content into individual key blocks
+        key_blocks = []
+        current_block = []
+        in_key_block = False
+
         for line in stdout.splitlines():
-            if keyid in line.split(':')[4]:
-                return True
+            if line.strip() == '-----BEGIN PGP PUBLIC KEY BLOCK-----':
+                in_key_block = True
+                current_block = [line]
+            elif line.strip() == '-----END PGP PUBLIC KEY BLOCK-----':
+                current_block.append(line)
+                key_blocks.append('\n'.join(current_block))
+                current_block = []
+                in_key_block = False
+            elif in_key_block:
+                current_block.append(line)
+
+        for armor_string in key_blocks:
+            installed_keys.extend(self.librpm.identify_keys(armor_string))
+
+        return installed_keys
+
+    def is_key_imported(self, keyid: str) -> bool:
+        """Check the supplied key ID value against the currently installed 
keys."""
+        keyid_len = len(keyid)
+        if keyid in [k['keyid'][-keyid_len:] for k in self.installed_keys]:
+            return True
+
+        # Allow the user supplied key to also be a fingerprint
+        if keyid in [f['fingerprint'] for f in self.installed_keys]:
+            return True
+
         return False
 
     def import_key(self, keyfile):
         if not self.module.check_mode:
             self.execute_command([self.rpm, '--import', keyfile])
 
-    def drop_key(self, keyid):
+    def _drop_key_rpm6(self, keyid: str) -> None:
+        """
+        Remove the key with the given key ID from the keyring using RPM 6+ 
method.
+
+        RPM version 6+ uses fingerprints and the 'rpmkeys --delete' command.
+        """
+        fingerprints = []
+        keyid_len = len(keyid)
+
+        for installed in self.installed_keys:
+            if keyid == installed['keyid'][-keyid_len:]:
+                fingerprints.append(installed['fingerprint'])
+            # We allow the user supplied 'key' to also be the full fingerprint.
+            elif keyid == installed['fingerprint']:
+                fingerprints.append(installed['fingerprint'])
+
+        if not fingerprints:
+            self.module.fail_json(msg=f"Supplied key ID {keyid} is not 
installed.")
+        elif len(fingerprints) == 1:
+            self.execute_command([self.rpmkeys, '--delete', fingerprints[0]])
+        else:
+            self.module.fail_json(msg=f"Supplied key ID {keyid} matches more 
than one fingerprint. Try using the fingerprint instead.")
+
+    def _drop_key_rpm4(self, keyid: str) -> None:
+        """
+        Remove the key with the given key ID from the keyring using RPM 4 
method.
+
+        Older RPM versions use short form key ID (4-bytes) and 'rpm --erase' 
command.
+        """
+        # If keyid is actually a fingerprint, we need to get the associated 
key ID and use it.
+        for installed in self.installed_keys:
+            if keyid == installed['fingerprint']:
+                keyid = installed['keyid']
+                break
+
+        self.execute_command([self.rpm, '--erase', '--allmatches', 
"gpg-pubkey-%s" % keyid[-8:].lower()])
+
+    def drop_key(self, keyid: str) -> None:
+        """Remove the key with the given key ID from the keyring."""
         if not self.module.check_mode:
-            self.execute_command([self.rpm, '--erase', '--allmatches', 
"gpg-pubkey-%s" % keyid[-8:].lower()])
+            if self.librpm.using_librpm6:
+                self._drop_key_rpm6(keyid)
+            else:
+                self._drop_key_rpm4(keyid)
 
 
 def main():
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ansible_core-2.18.14/lib/ansible/release.py 
new/ansible_core-2.18.15/lib/ansible/release.py
--- old/ansible_core-2.18.14/lib/ansible/release.py     2026-02-23 
23:49:04.000000000 +0100
+++ new/ansible_core-2.18.15/lib/ansible/release.py     2026-03-23 
18:38:33.000000000 +0100
@@ -17,6 +17,6 @@
 
 from __future__ import annotations
 
-__version__ = '2.18.14'
+__version__ = '2.18.15'
 __author__ = 'Ansible, Inc.'
 __codename__ = "Fool in the Rain"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ansible_core-2.18.14/pyproject.toml 
new/ansible_core-2.18.15/pyproject.toml
--- old/ansible_core-2.18.14/pyproject.toml     2026-02-23 23:49:04.000000000 
+0100
+++ new/ansible_core-2.18.15/pyproject.toml     2026-03-23 18:38:33.000000000 
+0100
@@ -1,5 +1,5 @@
 [build-system]
-requires = ["setuptools >= 66.1.0, <= 82.0.0", "wheel == 0.45.1"]  # lower 
bound to support controller Python versions, upper bound for latest version 
tested at release
+requires = ["setuptools >= 66.1.0, <= 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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/ansible_core-2.18.14/test/integration/targets/rpm_key/files/TEST-V6-NON-PQC-KEY.cert
 
new/ansible_core-2.18.15/test/integration/targets/rpm_key/files/TEST-V6-NON-PQC-KEY.cert
--- 
old/ansible_core-2.18.14/test/integration/targets/rpm_key/files/TEST-V6-NON-PQC-KEY.cert
    1970-01-01 01:00:00.000000000 +0100
+++ 
new/ansible_core-2.18.15/test/integration/targets/rpm_key/files/TEST-V6-NON-PQC-KEY.cert
    2026-03-23 18:38:33.000000000 +0100
@@ -0,0 +1,27 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Comment: 451A 6DBE 3B0F 51EA FBEF 3585 D53F 0277  62A6 C0DE A0A8 4EE5 7DB3 
87B0 1651 4BBC
+Comment: <[email protected]>
+Comment: Ansible Example
+
+xioGaWachxsAAAAgPh2gtnpD585bPVCmyST0RA3XAn6rc0oP4lTMSZ5GjQXCrAYf
+GwoAAAA9BYJpZpyHBYkFpI+9AwsJBwMVCggCmwECHgkiIQZFGm2+Ow9R6vvvNYXV
+PwJ3YqbA3qCoTuV9s4ewFlFLvAAAAAADziCYy5PwGbFr0+ZCRuWbw54j5otd0SrF
+h5zJih5TVryL1PNLMtQZt1XtLICOSBrtgbCMIhntaxW4O6ZHmv2p4BQIjt7kNWZA
+/AJcfn57iGdoFrZD8pMTWKYR66R+FForhwXNETxpbmZvQHJlZGhhdC5jb20+wqwG
+ExsKAAAAPQWCaWachwWJBaSPvQMLCQcDFQoIApsBAh4JIiEGRRptvjsPUer77zWF
+1T8Cd2KmwN6gqE7lfbOHsBZRS7wAAAAA+NogVNCvF3Y5SSpUF4cyH9NGRE9Gjdml
+KVefqvmh4T0g0JyM/FdRmxgkzRTaUdqjRmVMLg53ScY0AwaGjuhiEjVBe2GGQrtu
+aiupfyGvcAnUxNwmhoHvh0BeOu3vQwlCk14IzQ9BbnNpYmxlIEV4YW1wbGXCrwYT
+GwoAAABABYJpZpyHBYkFpI+9AwsJBwMVCggCmQECmwECHgkiIQZFGm2+Ow9R6vvv
+NYXVPwJ3YqbA3qCoTuV9s4ewFlFLvAAAAAAowSC8pmjMJom99WiMkM19I0qzNbFm
+YwCTpfirAk4v89ggIUw6eTF1oKlk6WTUeVeK2nBGHd2cZlV6NtKAlCllPqLq0rlp
+4ZxVo6cfUKxNjdnbTrE4Seamc0hYtLG49TEkaATOKgZpZpyHGwAAACAGbi2A12S/
+VoxdCm9I5VPSmsUHRO0DznHeB/VEG8NKCsLAewYYGwoAAADMBYJpZpyHBYkFpI+9
+ApsCmaAGGRsKAAAAKQWCaWachyIhBgiuhwe1s82YIvPJXs0dT6zFgNVk7juLtL8s
+Zx6H2pNDAAAAAILuILtOYzI43a9SImbCmkWStIxesTFKhFBMvaMPJOrErcWGjiga
+Ikq7NVpmSfyVs5s0DJrr7c3TJ0+Fneku/PCnppCh8140qfhlKVx+ePwpv41jQcK9
+uj8jTdK3I8Pn6yRGAyIhBkUabb47D1Hq++81hdU/AndipsDeoKhO5X2zh7AWUUu8
+AAAAAIufIPo2sGq+KZSbp1QV4/A65DtPpSK0oSwML/B4OrPQLQ8+9iuZ2CB5iHJ6
+4xQ4OvyKGslJU6fY0A23WgFguZdIvfIAvRLo2WqFkOhyBzJc55u5IU5ukGWn8AY0
+bgG7sR6hBA==
+-----END PGP PUBLIC KEY BLOCK-----
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/ansible_core-2.18.14/test/integration/targets/rpm_key/tasks/main.yaml 
new/ansible_core-2.18.15/test/integration/targets/rpm_key/tasks/main.yaml
--- old/ansible_core-2.18.14/test/integration/targets/rpm_key/tasks/main.yaml   
2026-02-23 23:49:04.000000000 +0100
+++ new/ansible_core-2.18.15/test/integration/targets/rpm_key/tasks/main.yaml   
2026-03-23 18:38:33.000000000 +0100
@@ -1,7 +1,3 @@
- - 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:
 
@@ -23,8 +19,10 @@
 
    always:
 
+   # This will fail if the tests leave no keys in the key store
    - name: Remove all GPG keys from key ring
      shell: rpm -q gpg-pubkey | xargs rpm -e
+     ignore_errors: yes
 
    - name: Restore the previously installed GPG keys
      command: rpm --import {{ (remote_tmp_dir + '/pubkeys') | quote }}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/ansible_core-2.18.14/test/integration/targets/rpm_key/tasks/rpm_key.yaml 
new/ansible_core-2.18.15/test/integration/targets/rpm_key/tasks/rpm_key.yaml
--- 
old/ansible_core-2.18.14/test/integration/targets/rpm_key/tasks/rpm_key.yaml    
    2026-02-23 23:49:04.000000000 +0100
+++ 
new/ansible_core-2.18.15/test/integration/targets/rpm_key/tasks/rpm_key.yaml    
    2026-03-23 18:38:33.000000000 +0100
@@ -123,6 +123,16 @@
   assert:
     that: key_id_idempotence is not changed
 
+- name: Add key to test deleting by fingerprint
+  rpm_key:
+    state: present
+    key: "{{ test_key_url }}"
+
+- name: Remove key by using fingerprint
+  rpm_key:
+    state: absent
+    key: "{{ primary_fingerprint }}"
+
 - name: Add very first key on system again
   rpm_key:
     state: present
@@ -227,3 +237,68 @@
     that:
       - result is success
       - result is not changed
+
+# Test PGPv6 keys
+- name: Test PGPv6 Keys
+  block:
+
+    # Reset to test PGPv6 keys validation
+    - name: Remove all keys from key ring
+      shell: rpm -q gpg-pubkey | xargs rpm -e
+
+    - name: Copy v6 key to remote
+      copy:
+        src: "files/{{ test_v6_key_file }}"
+        dest: "{{ remote_tmp_dir }}/{{ test_v6_key_file }}"
+        mode: "0644"
+
+    - name: Import v6 key
+      rpm_key:
+        state: present
+        key: "{{ remote_tmp_dir }}/{{ test_v6_key_file }}"
+        fingerprint: "{{ invalid_fingerprint }}"
+      register: result
+      failed_when: result is success
+
+    - name: Verify invalid fingerprint failure
+      assert:
+        that:
+           - result is success
+           - result is not changed
+           - result.msg is contains 'does not match any key fingerprints'
+
+    - name: Import v6 key
+      rpm_key:
+        state: present
+        key: "{{ remote_tmp_dir }}/{{ test_v6_key_file }}"
+        fingerprint: "{{ test_v6_key_fingerprint }}"
+
+    - name: Import v6 key (idempotent)
+      rpm_key:
+        state: present
+        key: "{{ remote_tmp_dir }}/{{ test_v6_key_file }}"
+        fingerprint: "{{ test_v6_key_fingerprint }}"
+      register: key_idempotence
+
+    - name: Verify idempotence
+      assert:
+        that: key_idempotence is not changed
+
+    - name: Delete v6 key
+      rpm_key:
+        state: absent
+        key: "{{ test_v6_key_keyid }}"
+
+    - name: Import v6 key again
+      rpm_key:
+        state: present
+        key: "{{ remote_tmp_dir }}/{{ test_v6_key_file }}"
+        fingerprint: "{{ test_v6_key_fingerprint }}"
+
+    - name: Delete v6 key by fingerprint
+      rpm_key:
+        state: absent
+        key: "{{ test_v6_key_fingerprint }}"
+
+  when: (ansible_facts['distribution'] == "RedHat" and 
ansible_facts['distribution_version'] is version('10.1', '>='))
+        or (ansible_facts['distribution'] == "Fedora" and 
ansible_facts['distribution_version'] is version('43', '>='))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/ansible_core-2.18.14/test/integration/targets/rpm_key/vars/main.yml 
new/ansible_core-2.18.15/test/integration/targets/rpm_key/vars/main.yml
--- old/ansible_core-2.18.14/test/integration/targets/rpm_key/vars/main.yml     
2026-02-23 23:49:04.000000000 +0100
+++ new/ansible_core-2.18.15/test/integration/targets/rpm_key/vars/main.yml     
2026-03-23 18:38:33.000000000 +0100
@@ -6,3 +6,6 @@
 invalid_fingerprint: 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111
 primary_fingerprint: 66D1 5FDD 8728 7219 C8E1 5478 D200 CD70 2853 E6D0
 sub_key_fingerprint: E617 DCD4 065C 2AFC 0B2C F7A7 BA8B C08C 0F69 1F94
+test_v6_key_file: "TEST-V6-NON-PQC-KEY.cert"
+test_v6_key_fingerprint: 
"451A6DBE3B0F51EAFBEF3585D53F027762A6C0DEA0A84EE57DB387B016514BBC"
+test_v6_key_keyid: "451A6DBE3B0F51EA"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/ansible_core-2.18.14/test/lib/ansible_test/_data/completion/docker.txt 
new/ansible_core-2.18.15/test/lib/ansible_test/_data/completion/docker.txt
--- old/ansible_core-2.18.14/test/lib/ansible_test/_data/completion/docker.txt  
2026-02-23 23:49:04.000000000 +0100
+++ new/ansible_core-2.18.15/test/lib/ansible_test/_data/completion/docker.txt  
2026-03-23 18:38:33.000000000 +0100
@@ -1,7 +1,7 @@
 base image=quay.io/ansible/base-test-container:7.7.0 
python=3.13,3.8,3.9,3.10,3.11,3.12
 default image=quay.io/ansible/default-test-container:10.9.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:10.9.0 
python=3.13,3.8,3.9,3.10,3.11,3.12 context=ansible-core
-alpine320 image=quay.io/ansible/alpine320-test-container:8.1.0 python=3.12 
cgroup=none audit=none
-fedora40 image=quay.io/ansible/fedora40-test-container:8.1.0 python=3.12
+alpine320 image=quay.io/ansible/alpine320-test-container:8.1.0 python=3.12 
cgroup=none audit=none alias=alpine
+fedora40 image=quay.io/ansible/fedora40-test-container:8.1.0 python=3.12 
alias=fedora
 ubuntu2204 image=quay.io/ansible/ubuntu2204-test-container:8.1.0 python=3.10
-ubuntu2404 image=quay.io/ansible/ubuntu2404-test-container:8.1.0 python=3.12
+ubuntu2404 image=quay.io/ansible/ubuntu2404-test-container:8.1.0 python=3.12 
alias=ubuntu
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/ansible_core-2.18.14/test/lib/ansible_test/_data/completion/remote.txt 
new/ansible_core-2.18.15/test/lib/ansible_test/_data/completion/remote.txt
--- old/ansible_core-2.18.14/test/lib/ansible_test/_data/completion/remote.txt  
2026-02-23 23:49:04.000000000 +0100
+++ new/ansible_core-2.18.15/test/lib/ansible_test/_data/completion/remote.txt  
2026-03-23 18:38:33.000000000 +0100
@@ -1,15 +1,15 @@
-alpine/3.20 python=3.12 become=doas_sudo provider=aws arch=x86_64
+alpine/3.20 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/40 python=3.12 become=sudo provider=aws arch=x86_64
+fedora/40 python=3.12 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.1 python=3.9,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.1 python=3.9,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/14.3 python=3.11 python_dir=/usr/local/bin become=sudo 
provider=parallels arch=x86_64
+macos/14.3 python=3.11 python_dir=/usr/local/bin become=sudo 
provider=parallels arch=x86_64 alias=macos/14,macos/latest
 macos python_dir=/usr/local/bin become=sudo provider=parallels arch=x86_64
-rhel/9.7 python=3.9,3.12 become=sudo provider=aws arch=x86_64
-rhel/10.1 python=3.12 become=sudo provider=aws arch=x86_64
+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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/ansible_core-2.18.14/test/lib/ansible_test/_data/completion/windows.txt 
new/ansible_core-2.18.15/test/lib/ansible_test/_data/completion/windows.txt
--- old/ansible_core-2.18.14/test/lib/ansible_test/_data/completion/windows.txt 
2026-02-23 23:49:04.000000000 +0100
+++ new/ansible_core-2.18.15/test/lib/ansible_test/_data/completion/windows.txt 
2026-03-23 18:38:33.000000000 +0100
@@ -1,4 +1,4 @@
 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/2022 provider=aws arch=x86_64 connection=winrm+https 
alias=windows/latest
 windows provider=aws arch=x86_64 connection=winrm+https
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/ansible_core-2.18.14/test/lib/ansible_test/_internal/ci/gha.py 
new/ansible_core-2.18.15/test/lib/ansible_test/_internal/ci/gha.py
--- old/ansible_core-2.18.14/test/lib/ansible_test/_internal/ci/gha.py  
1970-01-01 01:00:00.000000000 +0100
+++ new/ansible_core-2.18.15/test/lib/ansible_test/_internal/ci/gha.py  
2026-03-23 18:38:33.000000000 +0100
@@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/ansible_core-2.18.14/test/lib/ansible_test/_internal/cli/compat.py 
new/ansible_core-2.18.15/test/lib/ansible_test/_internal/cli/compat.py
--- old/ansible_core-2.18.14/test/lib/ansible_test/_internal/cli/compat.py      
2026-02-23 23:49:04.000000000 +0100
+++ new/ansible_core-2.18.15/test/lib/ansible_test/_internal/cli/compat.py      
2026-03-23 18:38:33.000000000 +0100
@@ -19,7 +19,6 @@
     display,
     filter_args,
     sorted_versions,
-    str_to_version,
 )
 
 from ..docker_util import (
@@ -30,6 +29,7 @@
     docker_completion,
     remote_completion,
     filter_completion,
+    windows_completion,
 )
 
 from ..host_configs import (
@@ -68,9 +68,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
 
 
@@ -350,17 +348,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:
@@ -385,23 +383,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}')
@@ -470,8 +470,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:
@@ -495,6 +495,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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/ansible_core-2.18.14/test/lib/ansible_test/_internal/cli/environments.py 
new/ansible_core-2.18.15/test/lib/ansible_test/_internal/cli/environments.py
--- 
old/ansible_core-2.18.14/test/lib/ansible_test/_internal/cli/environments.py    
    2026-02-23 23:49:04.000000000 +0100
+++ 
new/ansible_core-2.18.15/test/lib/ansible_test/_internal/cli/environments.py    
    2026-03-23 18:38:33.000000000 +0100
@@ -593,10 +593,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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/ansible_core-2.18.14/test/lib/ansible_test/_internal/completion.py 
new/ansible_core-2.18.15/test/lib/ansible_test/_internal/completion.py
--- old/ansible_core-2.18.14/test/lib/ansible_test/_internal/completion.py      
2026-02-23 23:49:04.000000000 +0100
+++ new/ansible_core-2.18.15/test/lib/ansible_test/_internal/completion.py      
2026-03-23 18:38:33.000000000 +0100
@@ -5,6 +5,8 @@
 import dataclasses
 import enum
 import os
+import re
+import sys
 import typing as t
 
 from .constants import (
@@ -16,6 +18,8 @@
     ANSIBLE_TEST_DATA_ROOT,
     cache,
     read_lines_without_comments,
+    str_to_version,
+    InternalError,
 )
 
 from .data import (
@@ -60,6 +64,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):
@@ -113,6 +122,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]
@@ -175,6 +194,19 @@
     placeholder: bool = False
 
     @property
+    def sort_key(self) -> tuple[str, tuple[int, ...]]:
+        """Key used for sorting completion entries."""
+        match = re.match('^(?P<platform>[a-z]+)(?P<version>[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
@@ -262,12 +294,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
 

++++++ ansible_core-2.18.14.tar.gz.sha256 -> ansible_core-2.18.15.tar.gz.sha256 
++++++
--- 
/work/SRC/openSUSE:Factory/ansible-core-2.18/ansible_core-2.18.14.tar.gz.sha256 
    2026-03-11 20:58:47.790525539 +0100
+++ 
/work/SRC/openSUSE:Factory/.ansible-core-2.18.new.8177/ansible_core-2.18.15.tar.gz.sha256
   2026-03-27 06:36:03.036701761 +0100
@@ -1 +1 @@
-da9ff29f673e2cf2632ddd266883977b6602559c65cbd0cfc4d48e70306e85d5  
ansible_core-2.18.14.tar.gz
+577effbe1dba09cecc2b03ca767f693b3f16a773d5c3cbdb8348a633b41f3e72  
ansible_core-2.18.15.tar.gz

Reply via email to