Repository: fedora-infra/bodhi

Issue:
RFE: move a build from unpushed update to another existing update
It would be good to have a way to merge updates together when independent people have accidentally submitted them as separate updates when they do need to land together.

The most recent case of this was with [FEDORA-2023-1b4954b53b](https://bodhi.fedoraproject.org/updates/FEDORA-2023-1b4954b53b) and [FEDORA-2023-a8847a9386](https://bodhi.fedoraproject.org/updates/FEDORA-2023-a8847a9386), where the former needed to be merged into the latter because they needed to be shipped together.

Unfortunately, there was no way to do it other than for me to do a no-change rebuild, obsolete the old one manually, and add the new build to the correct update.

This was a waste of human and compute resources that could have simply been avoided by having the ability to merge updates.

(Thankfully, this was only wallpapers, but I shudder if we had this problem with chromium or some other time-consuming package.)


Discussion:
Alternatively, having a way to remove all builds attached to an update would also work - that way, we could re-attach the builds to another update. It used to be possible to do that by deleting Bodhi updates, but that functionality was removed a while back.
Same as #842 

BTW you don't need to do a no-change rebuild, just unpush the former update, then the build become available to be attached to another update. See #3975
I tried that, it did not work.
@mattiaverga Oh wow, I had no idea that it should work like that - let me give this a try and see if I can merge https://bodhi.fedoraproject.org/updates/FEDORA-2023-b2e6ab4817 into https://bodhi.fedoraproject.org/updates/FEDORA-2023-d422824191

@Conan-Kudo I think you obsoleted the update, but didn't unpush. That may be a crucial difference.
I did the obsoleting later after I found unpushing didn't work.
OK, I tried with my two updates and it didn't work either.

What I did was first, `bodhi updates request FEDORA-2023-b2e6ab4817 unpush` from the command line, and then tried to add vala-0.56.13-1.fc39 from that update to https://bodhi.fedoraproject.org/updates/FEDORA-2023-d422824191 in the web UI.

The web UI errored out with:
```
Builds : Update for vala-0.56.13-1.fc39 already exists
```

I'll just leave them in this state for now if you want to experiment with them in some way.
Yeah, the commit I linked refers to create a new update with a build from an unpushed update.
It doesn't permit to edit an existing update with a build from an unpushed update... I'll see if this can be allowed without breaking things.
Thanks!

Resolving patch:
diff --git a/bodhi-server/bodhi/server/validators.py b/bodhi-server/bodhi/server/validators.py
--- a/bodhi-server/bodhi/server/validators.py
+++ b/bodhi-server/bodhi/server/validators.py
@@ -286,19 +286,21 @@ def validate_builds(request, **kwargs):
         for nvr in builds:
             # Ensure it doesn't already exist in another update
             build = request.db.query(Build).filter_by(nvr=nvr).first()
-            if build and build.update is not None and up.alias != build.update.alias:
+            if (build and build.update is not None and up.alias != build.update.alias
+                    and build.update.status != UpdateStatus.unpushed):
                 request.errors.add('body', 'builds',
                                    "Update for {} already exists".format(nvr))
+                return
 
         return
 
     for nvr in builds:
         build = request.db.query(Build).filter_by(nvr=nvr).first()
-        if build and build.update is not None:
-            if build.update.status != UpdateStatus.unpushed:
-                request.errors.add('body', 'builds',
-                                   "Update for {} already exists".format(nvr))
-                return
+        if (build and build.update is not None
+                and build.update.status != UpdateStatus.unpushed):
+            request.errors.add('body', 'builds',
+                               "Update for {} already exists".format(nvr))
+            return
 
 
 @postschema_validator


Test patch:
diff --git a/bodhi-server/tests/services/test_updates.py b/bodhi-server/tests/services/test_updates.py
--- a/bodhi-server/tests/services/test_updates.py
+++ b/bodhi-server/tests/services/test_updates.py
@@ -5998,6 +5998,76 @@ def test_edit_testing_update_with_build_from_different_update(self, *args):
         assert len(up.builds) == 1
         assert up.builds[0].nvr == nvr1
 
+    @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_build_from_unpushed_update(self, *args):
+        """
+        Allow to move a build from an unpushed update into another existing update
+        """
+        # Create an update with a build that we will try and add to another update
+        nvr1 = 'bodhi-2.0.0-2.fc17'
+        args = self.get_update(nvr1)
+
+        with fml_testing.mock_sends(update_schemas.UpdateRequestTestingV1):
+            r = self.app.post_json('/updates/', args)
+
+        # Mark it as unpushed
+        upd = Update.get(r.json['alias'])
+        unpushed_alias = upd.alias
+        upd.status = UpdateStatus.unpushed
+        upd.request = None
+        # Clear pending messages
+        self.db.info['messages'] = []
+        self.db.commit()
+
+        # Create an update for a different build
+        nvr2 = 'koji-2.0.0-1.fc17'
+        args = self.get_update(nvr2)
+
+        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.request = None
+        # Clear pending messages
+        self.db.info['messages'] = []
+        self.db.commit()
+
+        # Edit the nvr2 update and add nvr1
+        args['edited'] = upd.alias
+        args['builds'] = '%s,%s' % (nvr1, nvr2)
+        with fml_testing.mock_sends(update_schemas.UpdateRequestTestingV1,
+                                    update_schemas.UpdateEditV2):
+            r = self.app.post_json('/updates/', args)
+        up = r.json_body
+        assert len(up['builds']) == 2
+        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-2.fc17
+
+        Karma has been reset.
+        """).strip()
+        assert_multiline_equal(up['comments'][-2]['text'], comment)
+
+        # The newer update should have both builds
+        up = Update.get(upd.alias)
+        assert up.status == UpdateStatus.pending
+        assert up.request == UpdateRequest.testing
+        assert len(up.builds) == 2
+
+        # The unpushed update should have no builds
+        unpushed_update = Update.get(unpushed_alias)
+        unpushed_update.title == unpushed_alias
+        assert unpushed_update.status == UpdateStatus.unpushed
+        assert len(unpushed_update.builds) == 0
+
     @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)