From 19aeef4e0d6169fb7f566fc0ff3367a4ea5ea5fe Mon Sep 17 00:00:00 2001
From: Mike McLean <mikem@redhat.com>
Date: Jan 27 2026 22:52:50 +0000
Subject: [PATCH 1/2] catch nvra dups on promote


Fixes https://pagure.io/koji/issue/4530

---

diff --git a/kojihub/kojihub.py b/kojihub/kojihub.py
index 4c7956c..c341dae 100644
--- a/kojihub/kojihub.py
+++ b/kojihub/kojihub.py
@@ -10813,6 +10813,16 @@ def _promote_build(build, force=False):
             f"Target build exists: {old_build['nvr']}(#{old_build['id']})"
         ))
 
+    # check for nvra conflicts
+    for rpminfo in list_rpms(buildID=binfo['id']):
+        _rinfo = dslice(rpminfo, ('name', 'version', 'release', 'arch'))
+        dup = get_rpm(_rinfo)
+        if dup and (not dup['draft'] and not dup['external_repo_id']):
+            nvra = "%(name)s-%(version)s-%(release)s.%(arch)s" % dup
+            raise koji.GenericError(err_fmt.format(
+                f"NVRA conflict: {nvra} (#{dup['id']})"
+            ))
+
     # policy checks
     policy_data = {
         'build': binfo['id'],
diff --git a/tests/test_hub/test_promote_build.py b/tests/test_hub/test_promote_build.py
index 2405de8..672f7d5 100644
--- a/tests/test_hub/test_promote_build.py
+++ b/tests/test_hub/test_promote_build.py
@@ -41,6 +41,8 @@ class TestPromoteBuild(unittest.TestCase):
         self.os_symlink = mock.patch('os.symlink').start()
         self.list_tags = mock.patch('kojihub.kojihub.list_tags',
                                     return_value=[{'id': 101}]).start()
+        self.list_rpms = mock.patch('kojihub.kojihub.list_rpms', return_value=[]).start()
+        self.get_rpm = mock.patch('kojihub.kojihub.get_rpm', return_value=None).start()
         self.set_tag_update = mock.patch('kojihub.kojihub.set_tag_update').start()
         self._now = datetime.datetime.now()
         self._datetime = mock.patch('kojihub.kojihub.datetime.datetime').start()
@@ -178,6 +180,28 @@ class TestPromoteBuild(unittest.TestCase):
             'release': 'tgtrel'
         }, strict=False)
 
+    def test_promote_build_nvra_exists(self):
+        self.get_build.side_effect = [
+            self.draft_build,
+            None,
+        ]
+
+        rinfo = {
+            'name': 'N', 'version': 'V', 'release': 'R', 'arch': 'A',
+            'draft': False, 'external_repo_id': 0,
+            'id': 1234,
+        }
+        self.list_rpms.return_value = [rinfo.copy()]
+        self.get_rpm.return_value = rinfo.copy()
+
+        with self.assertRaises(koji.GenericError) as cm:
+            self.exports.promoteBuild('a-regular-build')
+        self.assertEqual(
+            str(cm.exception),
+            "Cannot promote build testnvr. Reason: NVRA conflict: N-V-R.A (#1234)"
+        )
+        self.assertEqual(len(self.updates), 0)
+
 
 class TestPromoteBuildFiles(unittest.TestCase):
     # these tests use a tempdir
@@ -219,6 +243,8 @@ class TestPromoteBuildFiles(unittest.TestCase):
                                       return_value={'id': 1, 'name': 'DEFAULT'}).start()
         self.list_tags = mock.patch('kojihub.kojihub.list_tags',
                                     return_value=[{'id': 101}]).start()
+        self.list_rpms = mock.patch('kojihub.kojihub.list_rpms', return_value=[]).start()
+        self.get_rpm = mock.patch('kojihub.kojihub.get_rpm', return_value=None).start()
         self.set_tag_update = mock.patch('kojihub.kojihub.set_tag_update').start()
         self._now = datetime.datetime.now()
         self._datetime = mock.patch('kojihub.kojihub.datetime.datetime').start()

From 73d92a872651171ce0e7c7c2ab62f412f5361e1a Mon Sep 17 00:00:00 2001
From: Mike McLean <mikem@redhat.com>
Date: Jan 27 2026 22:52:50 +0000
Subject: [PATCH 2/2] drop redundant parentheses


---

diff --git a/kojihub/kojihub.py b/kojihub/kojihub.py
index c341dae..b0a3d5a 100644
--- a/kojihub/kojihub.py
+++ b/kojihub/kojihub.py
@@ -10817,7 +10817,7 @@ def _promote_build(build, force=False):
     for rpminfo in list_rpms(buildID=binfo['id']):
         _rinfo = dslice(rpminfo, ('name', 'version', 'release', 'arch'))
         dup = get_rpm(_rinfo)
-        if dup and (not dup['draft'] and not dup['external_repo_id']):
+        if dup and not dup['draft'] and not dup['external_repo_id']:
             nvra = "%(name)s-%(version)s-%(release)s.%(arch)s" % dup
             raise koji.GenericError(err_fmt.format(
                 f"NVRA conflict: {nvra} (#{dup['id']})"

