Repository: fedora-infra/bodhi

Issue:
Stable updates push based on update id
During release freezes, Fedora QA will request Fedora RelEng to push some of the updates to push to stable if they fix blockers or FE's. For that we use

```
bodhi-push --builds 'nvr,nvr,...'
```

This is okay if the updates have only one build, but it becomes tiresome if there a multiple updates like [this update](https://bodhi.fedoraproject.org/updates/FEDORA-2019-ac2a21ff07) for example.

Can you make it to pass the update id's to push to stable? Something like

```
bodhi-push --updates 'update_id1,update_id2...'
```


Discussion:
Sure, but one note: I think you can put any one of the builds from the update in and I think it'll find it. (Not sure, I haven't tried that, but I'm guessing it'd work.)
I will give it a shot next time and I will let you know. 
This unittest shows that update with provided build via cli is locked after bodhi-push:
https://github.com/fedora-infra/bodhi/blob/b7e9a87b474fc7d008164a9e0df41963106abb87/bodhi/tests/server/test_push.py#L420
> Sure, but one note: I think you can put any one of the builds from the update in and I think it'll find it. (Not sure, I haven't tried that, but I'm guessing it'd work.)

This is working, so, this ticket is not critical, but good to have.

Resolving patch:
diff --git a/bodhi/server/push.py b/bodhi/server/push.py
--- a/bodhi/server/push.py
+++ b/bodhi/server/push.py
@@ -16,6 +16,8 @@
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 """The CLI tool for triggering update pushes."""
+import sys
+
 from sqlalchemy.sql import or_
 import click
 
@@ -50,8 +52,30 @@ def update_sig_status(update):
                 click.echo('Build %s still unsigned' % build.nvr)
 
 
+def check_if_updates_and_builds_set(
+        ctx: click.core.Context, param: click.core.Option, value: str) -> str:
+    """
+    Print an error to stderr if the user has set both the --updates and --builds flags.
+
+    Args:
+        ctx: The Click context, used to find out if the other flags are set.
+        param: The option being handled.
+        value: The value of the param flag.
+    Returns:
+        The value of the param flag.
+    """
+    if value is not None and ((param.name == 'builds' and ctx.params.get('updates', False))
+                              or (param.name == 'updates' and ctx.params.get('builds', False))):
+        click.echo('ERROR: Must specify only one of --updates or --builds')
+        sys.exit(1)
+    return value
+
+
 @click.command()
-@click.option('--builds', help='Push updates for a comma-separated list of builds')
+@click.option('--builds', help='Push updates for a comma-separated list of builds',
+              callback=check_if_updates_and_builds_set)
+@click.option('--updates', help='Push updates for a comma-separated list of update aliases',
+              callback=check_if_updates_and_builds_set)
 @click.option('--releases', help=('Push updates for a comma-separated list of releases (default: '
                                   'current and pending releases)'))
 @click.option('--request', default='testing,stable',
@@ -120,6 +144,10 @@ def push(username, yes, **kwargs):
                 query = query.filter(
                     or_(*[Build.nvr == build for build in kwargs['builds'].split(',')]))
 
+            if kwargs.get('updates'):
+                query = query.filter(
+                    or_(*[Update.alias == alias for alias in kwargs['updates'].split(',')]))
+
             query = _filter_releases(session, query, kwargs.get('releases'))
 
             for update in query.all():


Test patch:
diff --git a/bodhi/tests/server/test_push.py b/bodhi/tests/server/test_push.py
--- a/bodhi/tests/server/test_push.py
+++ b/bodhi/tests/server/test_push.py
@@ -364,6 +364,9 @@ def test_archived_release(self):
 Sending composer.start message
 """
 
+TEST_BUILDS_AND_UPDATES_FLAG_EXPECTED_OUTPUT = """ERROR: Must specify only one of --updates or --builds
+"""
+
 
 class TestPush(base.BaseTestCase):
     """
@@ -451,6 +454,71 @@ def test_builds_flag(self):
         self.assertFalse(python_paste_deploy.locked)
         self.assertIsNone(python_paste_deploy.date_locked)
 
+    def test_updates_flag(self):
+        """
+        Assert correct operation when the --updates flag is given.
+        """
+        cli = CliRunner()
+        ejabberd = self.create_update(['ejabberd-16.09-4.fc17'])
+        alias1 = ejabberd.alias
+        u = self.db.query(models.Build).filter_by(nvr='python-nose-1.3.7-11.fc17').one().update
+        alias2 = u.alias
+        # Make it so we have three builds we could push out so that we can ask for and verify two
+        ejabberd.builds[0].signed = True
+        self.db.commit()
+        expected_message = composer_schemas.ComposerStartV1.from_dict({
+            'composes': [{'security': False, 'release_id': ejabberd.release.id,
+                          'request': u'testing', 'content_type': u'rpm'}],
+            'resume': False, 'agent': 'bowlofeggs', 'api_version': 2})
+
+        with mock.patch('bodhi.server.push.transactional_session_maker',
+                        return_value=base.TransactionalSessionMaker(self.Session)):
+            with fml_testing.mock_sends(expected_message):
+                result = cli.invoke(
+                    push.push,
+                    ['--username', 'bowlofeggs', '--updates', alias1 + ',' + alias2],
+                    input='y')
+
+        self.assertEqual(result.exit_code, 0)
+        self.assertEqual(result.output, TEST_BUILDS_FLAG_EXPECTED_OUTPUT)
+        for nvr in ['ejabberd-16.09-4.fc17', 'python-nose-1.3.7-11.fc17']:
+            u = self.db.query(models.Build).filter_by(nvr=nvr).one().update
+            self.assertTrue(u.locked)
+            self.assertTrue(u.date_locked <= datetime.utcnow())
+        python_paste_deploy = self.db.query(models.Build).filter_by(
+            nvr='python-paste-deploy-1.5.2-8.fc17').one().update
+        self.assertFalse(python_paste_deploy.locked)
+        self.assertIsNone(python_paste_deploy.date_locked)
+
+    def test_updates_and_builds_flag(self):
+        """
+        Assert correct operation when --builds and --updates flags are given.
+        """
+        cli = CliRunner()
+        ejabberd = self.create_update(['ejabberd-16.09-4.fc17'])
+        alias = ejabberd.alias
+        self.db.commit()
+
+        with mock.patch('bodhi.server.push.transactional_session_maker',
+                        return_value=base.TransactionalSessionMaker(self.Session)):
+            with fml_testing.mock_sends():
+                result = cli.invoke(
+                    push.push,
+                    ['--username', 'bowlofeggs', '--builds', 'python-nose-1.3.7-11.fc17',
+                     '--updates', alias],
+                    input='y')
+
+        self.assertEqual(result.exit_code, 1)
+        self.assertEqual(result.output, TEST_BUILDS_AND_UPDATES_FLAG_EXPECTED_OUTPUT)
+        for nvr in [
+            'ejabberd-16.09-4.fc17',
+            'python-nose-1.3.7-11.fc17',
+            'python-paste-deploy-1.5.2-8.fc17',
+        ]:
+            u = self.db.query(models.Build).filter_by(nvr=nvr).one().update
+            self.assertFalse(u.locked)
+            self.assertIsNone(u.date_locked)
+
     def test_yes_flag(self):
         """
         Test correct operation when the --yes flag is used.