Repository: fedora-infra/bodhi

Issue:
Adding new builds to an update will let the update stuck
I think I've messed things up in #4261 and probably now when editing any update by adding new builds will let the update in a stale state because the `release-candidate` tag is not added to the new builds.

We (I) need to review my changes and fix this ASAP. And possibly find a way to test the tagging workflow when editing an update, which now isn't tested at all because there's no easy way to check that.

In the meantime, expect more and more infra tickets opened about "builds ejected from the compose"... sorry about that.


Discussion:

Resolving patch:
diff --git a/bodhi/server/models.py b/bodhi/server/models.py
--- a/bodhi/server/models.py
+++ b/bodhi/server/models.py
@@ -1482,12 +1482,13 @@ def get_creation_time(self) -> datetime:
         """Return the creation time of the build."""
         return datetime.fromisoformat(self._get_kojiinfo()['creation_time'])
 
-    def unpush(self, koji):
+    def unpush(self, koji, from_side_tag=False):
         """
         Move this build back to the candidate tag and remove any pending tags.
 
         Args:
             koji (bodhi.server.buildsys.Buildsysem or koji.ClientSession): A koji client.
+            from_side_tag (bool): if the build was originally built in a side-tag.
         """
         log.info('Unpushing %s' % self.nvr)
         release = self.update.release
@@ -1501,11 +1502,16 @@ def unpush(self, koji):
             if tag == release.pending_stable_tag:
                 log.info('Removing %s tag from %s' % (tag, self.nvr))
                 koji.untagBuild(tag, self.nvr)
-            elif tag == release.testing_tag:
-                log.info(
-                    'Moving %s from %s to %s' % (
-                        self.nvr, tag, release.candidate_tag))
-                koji.moveBuild(tag, release.candidate_tag, self.nvr)
+            if tag == release.testing_tag:
+                if not from_side_tag:
+                    log.info(f'Moving {self.nvr} from {tag} to {release.candidate_tag}')
+                    koji.moveBuild(tag, release.candidate_tag, self.nvr)
+                else:
+                    log.info(f'Removing {tag} tag from {self.nvr}')
+                    koji.untagBuild(tag, self.nvr)
+            elif from_side_tag and tag == release.candidate_tag:
+                log.info(f'Removing {tag} tag from {self.nvr}')
+                koji.untagBuild(tag, self.nvr)
 
     def is_latest(self) -> bool:
         """Check if this is the latest build available in the stable tag."""
@@ -2495,7 +2501,7 @@ def edit(cls, request, data):
                     if b.nvr == build:
                         break
 
-                b.unpush(koji=request.koji)
+                b.unpush(koji=request.koji, from_side_tag=bool(up.from_tag))
                 up.builds.remove(b)
 
                 # Expire any associated buildroot override
@@ -2545,25 +2551,33 @@ def edit(cls, request, data):
                     'sent back to testing.',
                 })
                 builds_to_tag = [b.nvr for b in up.builds]
+                if up.from_tag and not up.release.composed_by_bodhi:
+                    tags = [up.release.get_pending_signing_side_tag(up.from_tag)]
+                elif up.release.pending_signing_tag:
+                    tags = [up.release.pending_signing_tag]
             else:
                 # No need to unpush the update, just tag new builds
+                # into the appropriate pending-signing tag
                 builds_to_tag = new_builds
+                if up.from_tag and not up.release.composed_by_bodhi:
+                    tags = [up.release.get_pending_signing_side_tag(up.from_tag)]
+                elif up.from_tag:
+                    tags = [up.release.candidate_tag]
+                    if up.release.pending_signing_tag:
+                        tags.append(up.release.pending_signing_tag)
+                else:
+                    # For normal updates the builds already have candidate_tag
+                    if up.release.pending_signing_tag:
+                        tags = [up.release.pending_signing_tag]
 
-            # Add the pending_signing_tag to builds where needed
-            tag = None
-            if up.from_tag and not up.release.composed_by_bodhi:
-                tag = up.release.get_pending_signing_side_tag(up.from_tag)
-            elif up.release.pending_signing_tag:
-                tag = up.release.pending_signing_tag
-
-            if tag is not None:
+            for tag in tags:
                 tag_update_builds_task.delay(tag=tag, builds=builds_to_tag)
 
         new_bugs = up.update_bugs(data['bugs'], db)
         del(data['bugs'])
 
         req = data.pop("request", None)
-        if req is not None and up.release.composed_by_bodhi and not data.get("from_tag"):
+        if req is not None and up.release.composed_by_bodhi:
             up.set_request(db, req, request.user.name)
 
         for key, value in data.items():


Test patch:
diff --git a/bodhi/tests/server/services/test_updates.py b/bodhi/tests/server/services/test_updates.py
--- a/bodhi/tests/server/services/test_updates.py
+++ b/bodhi/tests/server/services/test_updates.py
@@ -3267,9 +3267,70 @@ def test_edit_rpm_update_from_tag(self, *args):
         assert self.db.query(RpmBuild).filter_by(nvr='bodhi-2.0.0-2.fc17').first() is None
 
     @mock.patch('bodhi.server.services.updates.handle_side_and_related_tags_task', mock.Mock())
-    @mock.patch('bodhi.server.models.tag_update_builds_task', mock.Mock())
     @mock.patch(**mock_valid_requirements)
-    def test_edit_testing_update_with_new_builds(self, *args):
+    @mock.patch('bodhi.server.models.tag_update_builds_task')
+    @mock.patch('bodhi.server.models.Build.get_tags')
+    @mock.patch('bodhi.server.models.log.info')
+    def test_edit_pending_update_with_new_builds(self, info, get_tags, tag_task, *args):
+        nvr = 'bodhi-2.0.0-2.fc17'
+        args = self.get_update(nvr)
+
+        with fml_testing.mock_sends(update_schemas.UpdateRequestTestingV1):
+            r = self.app.post_json('/updates/', args)
+
+        # Mark it as pending
+        upd = Update.get(r.json['alias'])
+        upd.status = UpdateStatus.pending
+        upd.request = UpdateRequest.testing
+        # Clear pending messages
+        self.db.info['messages'] = []
+        self.db.commit()
+
+        get_tags.return_value = ['f17-updates-candidate',
+                                 'f17-updates-testing-pending']
+
+        args['edited'] = upd.alias
+        args['builds'] = 'bodhi-2.0.0-3.fc17'
+
+        with fml_testing.mock_sends(update_schemas.UpdateEditV1):
+            r = self.app.post_json('/updates/', args)
+
+        info.assert_any_call('Unpushing bodhi-2.0.0-2.fc17')
+        info.assert_any_call('Removing f17-updates-testing-pending tag from bodhi-2.0.0-2.fc17')
+        assert mock.call(
+            'Removing f17-updates-candidate tag from bodhi-2.0.0-2.fc17'
+        ) not in info.call_args_list
+        tag_task.delay.assert_called_once_with(tag='f17-updates-signing-pending',
+                                               builds=['bodhi-2.0.0-3.fc17'])
+        up = r.json_body
+        assert up['title'] == 'bodhi-2.0.0-3.fc17'
+        assert up['status'] == 'pending'
+        assert up['request'] == 'testing'
+        comment = textwrap.dedent("""
+        guest edited this update.
+
+        New build(s):
+
+        - bodhi-2.0.0-3.fc17
+
+        Removed build(s):
+
+        - bodhi-2.0.0-2.fc17
+
+        Karma has been reset.
+        """).strip()
+        assert_multiline_equal(up['comments'][-1]['text'], comment)
+        assert up['comments'][-3]['text'] == 'This update has been submitted for testing by guest. '
+        assert len(up['builds']) == 1
+        assert up['builds'][0]['nvr'] == 'bodhi-2.0.0-3.fc17'
+        assert self.db.query(RpmBuild).filter_by(nvr='bodhi-2.0.0-2.fc17').first() is None
+
+    @mock.patch('bodhi.server.services.updates.handle_side_and_related_tags_task', mock.Mock())
+    @mock.patch(**mock_valid_requirements)
+    @mock.patch('bodhi.server.models.tag_update_builds_task')
+    @mock.patch('bodhi.server.models.Build.get_tags')
+    @mock.patch('bodhi.server.models.log.info')
+    def test_edit_testing_update_with_new_builds(self, info, get_tags, tag_task, *args):
         nvr = 'bodhi-2.0.0-2.fc17'
         args = self.get_update(nvr)
 
@@ -3284,6 +3345,8 @@ def test_edit_testing_update_with_new_builds(self, *args):
         self.db.info['messages'] = []
         self.db.commit()
 
+        get_tags.return_value = ['f17-updates-testing']
+
         args['edited'] = upd.alias
         args['builds'] = 'bodhi-2.0.0-3.fc17'
 
@@ -3291,6 +3354,11 @@ def test_edit_testing_update_with_new_builds(self, *args):
                                     update_schemas.UpdateEditV1):
             r = self.app.post_json('/updates/', args)
 
+        info.assert_any_call('Unpushing bodhi-2.0.0-2.fc17')
+        info.assert_any_call(
+            'Moving bodhi-2.0.0-2.fc17 from f17-updates-testing to f17-updates-candidate')
+        tag_task.delay.assert_called_once_with(tag='f17-updates-signing-pending',
+                                               builds=['bodhi-2.0.0-3.fc17'])
         up = r.json_body
         assert up['title'] == 'bodhi-2.0.0-3.fc17'
         assert up['status'] == 'pending'
@@ -3316,9 +3384,12 @@ def test_edit_testing_update_with_new_builds(self, *args):
         assert self.db.query(RpmBuild).filter_by(nvr='bodhi-2.0.0-2.fc17').first() is None
 
     @mock.patch('bodhi.server.services.updates.handle_side_and_related_tags_task', mock.Mock())
-    @mock.patch('bodhi.server.models.tag_update_builds_task', mock.Mock())
     @mock.patch(**mock_valid_requirements)
-    def test_edit_testing_update_with_new_builds_with_stable_request(self, *args):
+    @mock.patch('bodhi.server.models.tag_update_builds_task')
+    @mock.patch('bodhi.server.models.Build.get_tags')
+    @mock.patch('bodhi.server.models.log.info')
+    def test_edit_testing_update_with_new_builds_with_stable_request(self, info, get_tags,
+                                                                     tag_task, *args):
         nvr = 'bodhi-2.0.0-2.fc17'
         args = self.get_update(nvr)
 
@@ -3333,6 +3404,9 @@ def test_edit_testing_update_with_new_builds_with_stable_request(self, *args):
         self.db.info['messages'] = []
         self.db.commit()
 
+        get_tags.return_value = ['f17-updates-testing',
+                                 'f17-updates-pending']
+
         args['edited'] = upd.alias
         args['builds'] = 'bodhi-2.0.0-3.fc17'
 
@@ -3340,6 +3414,12 @@ def test_edit_testing_update_with_new_builds_with_stable_request(self, *args):
                                     update_schemas.UpdateEditV1):
             r = self.app.post_json('/updates/', args)
 
+        info.assert_any_call('Unpushing bodhi-2.0.0-2.fc17')
+        info.assert_any_call('Removing f17-updates-pending tag from bodhi-2.0.0-2.fc17')
+        info.assert_any_call(
+            'Moving bodhi-2.0.0-2.fc17 from f17-updates-testing to f17-updates-candidate')
+        tag_task.delay.assert_called_once_with(tag='f17-updates-signing-pending',
+                                               builds=['bodhi-2.0.0-3.fc17'])
         up = r.json_body
         assert up['title'] == 'bodhi-2.0.0-3.fc17'
         assert up['status'] == 'pending'
@@ -3400,6 +3480,126 @@ def test_edit_testing_update_with_stable_request_no_edit_build(self, *args):
         assert len(up['builds']) == 1
         assert up['builds'][0]['nvr'] == 'bodhi-2.0.0-2.fc17'
 
+    @mock.patch('bodhi.server.services.updates.handle_side_and_related_tags_task', mock.Mock())
+    @mock.patch(**mock_valid_requirements)
+    @mock.patch('bodhi.server.models.tag_update_builds_task')
+    @mock.patch('bodhi.server.models.Build.get_tags')
+    @mock.patch('bodhi.server.models.log.info')
+    def test_edit_pending_sidetag_update_with_new_builds(self, info, get_tags, tag_task, *args):
+        nvr = 'bodhi-2.0.0-2.fc17'
+        args = self.get_update(nvr)
+
+        with fml_testing.mock_sends(update_schemas.UpdateRequestTestingV1):
+            r = self.app.post_json('/updates/', args)
+
+        # Mark it as pending
+        upd = Update.get(r.json['alias'])
+        upd.status = UpdateStatus.pending
+        upd.request = UpdateRequest.testing
+        upd.from_tag = 'f17-build-side-12345'
+        # Clear pending messages
+        self.db.info['messages'] = []
+        self.db.commit()
+
+        get_tags.return_value = ['f17-updates-candidate',
+                                 'f17-updates-testing-pending',
+                                 'f17-build-side-12345']
+
+        args['edited'] = upd.alias
+        args['builds'] = 'bodhi-2.0.0-3.fc17'
+
+        with fml_testing.mock_sends(update_schemas.UpdateEditV1):
+            r = self.app.post_json('/updates/', args)
+
+        info.assert_any_call('Unpushing bodhi-2.0.0-2.fc17')
+        info.assert_any_call('Removing f17-updates-testing-pending tag from bodhi-2.0.0-2.fc17')
+        info.assert_any_call('Removing f17-updates-candidate tag from bodhi-2.0.0-2.fc17')
+        tag_task.delay.assert_any_call(tag='f17-updates-signing-pending',
+                                       builds=['bodhi-2.0.0-3.fc17'])
+        tag_task.delay.assert_any_call(tag='f17-updates-candidate',
+                                       builds=['bodhi-2.0.0-3.fc17'])
+        up = r.json_body
+        assert up['title'] == 'bodhi-2.0.0-3.fc17'
+        assert up['status'] == 'pending'
+        assert up['request'] == 'testing'
+        comment = textwrap.dedent("""
+        guest edited this update.
+
+        New build(s):
+
+        - bodhi-2.0.0-3.fc17
+
+        Removed build(s):
+
+        - bodhi-2.0.0-2.fc17
+
+        Karma has been reset.
+        """).strip()
+        assert_multiline_equal(up['comments'][-1]['text'], comment)
+        assert up['comments'][-3]['text'] == 'This update has been submitted for testing by guest. '
+        assert len(up['builds']) == 1
+        assert up['builds'][0]['nvr'] == 'bodhi-2.0.0-3.fc17'
+        assert self.db.query(RpmBuild).filter_by(nvr='bodhi-2.0.0-2.fc17').first() is None
+
+    @mock.patch('bodhi.server.services.updates.handle_side_and_related_tags_task', mock.Mock())
+    @mock.patch(**mock_valid_requirements)
+    @mock.patch('bodhi.server.models.tag_update_builds_task')
+    @mock.patch('bodhi.server.models.Build.get_tags')
+    @mock.patch('bodhi.server.models.log.info')
+    def test_edit_testing_sidetag_update_with_new_builds(self, info, get_tags, tag_task, *args):
+        nvr = 'bodhi-2.0.0-2.fc17'
+        args = self.get_update(nvr)
+
+        with fml_testing.mock_sends(update_schemas.UpdateRequestTestingV1):
+            r = self.app.post_json('/updates/', args)
+
+        # Mark it as testing
+        upd = Update.get(r.json['alias'])
+        upd.status = UpdateStatus.testing
+        upd.from_tag = 'f17-build-side-12345'
+        upd.request = None
+        # Clear pending messages
+        self.db.info['messages'] = []
+        self.db.commit()
+
+        get_tags.return_value = ['f17-updates-testing',
+                                 'f17-build-side-12345']
+
+        args['edited'] = upd.alias
+        args['builds'] = 'bodhi-2.0.0-3.fc17'
+
+        with fml_testing.mock_sends(update_schemas.UpdateRequestTestingV1,
+                                    update_schemas.UpdateEditV1):
+            r = self.app.post_json('/updates/', args)
+
+        info.assert_any_call('Unpushing bodhi-2.0.0-2.fc17')
+        info.assert_any_call('Removing f17-updates-testing tag from bodhi-2.0.0-2.fc17')
+        tag_task.delay.assert_any_call(tag='f17-updates-signing-pending',
+                                       builds=['bodhi-2.0.0-3.fc17'])
+        up = r.json_body
+        assert up['title'] == 'bodhi-2.0.0-3.fc17'
+        assert up['status'] == 'pending'
+        assert up['request'] == 'testing'
+        assert up['comments'][-1]['text'] == 'This update has been submitted for testing by guest. '
+        comment = textwrap.dedent("""
+        guest edited this update.
+
+        New build(s):
+
+        - bodhi-2.0.0-3.fc17
+
+        Removed build(s):
+
+        - bodhi-2.0.0-2.fc17
+
+        Karma has been reset.
+        """).strip()
+        assert_multiline_equal(up['comments'][-2]['text'], comment)
+        assert up['comments'][-4]['text'] == 'This update has been submitted for testing by guest. '
+        assert len(up['builds']) == 1
+        assert up['builds'][0]['nvr'] == 'bodhi-2.0.0-3.fc17'
+        assert self.db.query(RpmBuild).filter_by(nvr='bodhi-2.0.0-2.fc17').first() is None
+
     @mock.patch('bodhi.server.services.updates.handle_side_and_related_tags_task', mock.Mock())
     @mock.patch('bodhi.server.models.tag_update_builds_task', mock.Mock())
     @mock.patch(**mock_valid_requirements)
diff --git a/bodhi/tests/server/test_models.py b/bodhi/tests/server/test_models.py
--- a/bodhi/tests/server/test_models.py
+++ b/bodhi/tests/server/test_models.py
@@ -3393,6 +3393,25 @@ def test_unpush_pending_stable(self):
             ('dist-f11-updates-testing-pending', 'TurboGears-1.0.8-3.fc11'),
             ('dist-f11-updates-pending', 'TurboGears-1.0.8-3.fc11')]
 
+    def test_unpush_pending_stable_from_sidetag(self):
+        """Test unpush() on a pending stable tagged build originated from side-tag."""
+        release = self.obj.release
+        build = self.obj.builds[0]
+        koji = buildsys.get_session()
+        koji.__tagged__[build.nvr] = [
+            release.testing_tag, release.pending_signing_tag, release.pending_testing_tag,
+            release.pending_stable_tag, 'f35-build-side-12345',
+            # Add an unknown tag that we shouldn't touch
+            release.dist_tag + '-compose']
+
+        build.unpush(koji, from_side_tag=True)
+
+        assert koji.__untag__ == [
+            ('dist-f11-updates-testing', 'TurboGears-1.0.8-3.fc11'),
+            ('dist-f11-updates-testing-signing', 'TurboGears-1.0.8-3.fc11'),
+            ('dist-f11-updates-testing-pending', 'TurboGears-1.0.8-3.fc11'),
+            ('dist-f11-updates-pending', 'TurboGears-1.0.8-3.fc11')]
+
     @mock.patch('bodhi.server.models.log.info')
     def test_unpush_update(self, info):
         """Unpushing an update shouldn't clear the override tag from builds."""