Repository: fedora-infra/bodhi

Issue:
Fold the functionality of bodhi-manage-releases into the Bodhi CLI
It is silly that Bodhi has a separate CLI to manage releases. This duplicates boilerplate code and also requires users to install Bodhi's server on their machine in order to perform administrative actions with its CLI. We should move all functionality from ```bodhi-manage-releases``` into the ```bodhi``` CLI under a new ```releases``` command.

In doing this, we should mark ```bodhi-manage-releases``` as deprecated in the release notes, in its man page, in its ```--help``` text, and have it print a deprecation warning when it is used.


Discussion:

Resolving patch:
diff --git a/bodhi/client/__init__.py b/bodhi/client/__init__.py
--- a/bodhi/client/__init__.py
+++ b/bodhi/client/__init__.py
@@ -28,6 +28,7 @@
 
 import click
 import six
+import munch
 
 from bodhi.client import bindings
 from fedora.client import AuthError
@@ -95,6 +96,7 @@ def _warn_if_url_and_staging_set(ctx, param, value):
     staging_option,
     url_option]
 
+
 # Basic options for pagination of query result
 pagination_options = [
     click.option('--rows', default=None,
@@ -105,6 +107,33 @@ def _warn_if_url_and_staging_set(ctx, param, value):
                  help='Go to page number')]
 
 
+# Common releases options
+release_options = [
+    click.option('--username'),
+    click.option('--password', prompt=True, hide_input=True),
+    click.option('--name', help='Release name (eg: F20)'),
+    click.option('--long-name', help='Long release name (eg: "Fedora 20")'),
+    click.option('--id-prefix', help='Release prefix (eg: FEDORA)'),
+    click.option('--version', help='Release version number (eg: 20)'),
+    click.option('--branch', help='Git branch name (eg: f20)'),
+    click.option('--dist-tag', help='Koji dist tag (eg: f20)'),
+    click.option('--stable-tag', help='Koji stable tag (eg: f20-updates)'),
+    click.option('--testing-tag',
+                 help='Koji testing tag (eg: f20-updates-testing)'),
+    click.option('--candidate-tag',
+                 help='Koji candidate tag (eg: f20-updates-candidate)'),
+    click.option('--pending-stable-tag',
+                 help='Koji pending tag (eg: f20-updates-pending)'),
+    click.option('--pending-testing-tag',
+                 help='Koji pending testing tag (eg: f20-updates-testing-testing)'),
+    click.option('--override-tag', help='Koji override tag (eg: f20-override)'),
+    click.option('--state', type=click.Choice(['disabled', 'pending', 'current',
+                                               'archived']),
+                 help='The state of the release'),
+    staging_option,
+    url_option]
+
+
 def add_options(options):
     """
     Generate a click.option decorator with the given options.
@@ -787,5 +816,138 @@ def print_resp(resp, client, verbose=False):
             click.echo(caveat.description)
 
 
+@cli.group()
+def releases():
+    # Docs that show in the --help
+    """Interact with releases."""
+    # Developer Docs
+    """Manage the releases."""
+    pass  # pragma: no cover
+
+
+@releases.command(name='create')
+@handle_errors
+@add_options(release_options)
+def create_release(username, password, url, **kwargs):
+    """Create a release."""
+    client = bindings.BodhiClient(base_url=url, username=username, password=password,
+                                  staging=kwargs['staging'])
+    kwargs['csrf_token'] = client.csrf()
+
+    save(client, **kwargs)
+
+
+@releases.command(name='edit')
+@handle_errors
+@add_options(release_options)
+@click.option('--new-name', help='New release name (eg: F20)')
+def edit_release(username, password, url, **kwargs):
+    """Edit an existing release."""
+    client = bindings.BodhiClient(base_url=url, username=username, password=password,
+                                  staging=kwargs['staging'])
+    csrf = client.csrf()
+
+    edited = kwargs.pop('name')
+
+    if edited is None:
+        print("ERROR: Please specify the name of the release to edit")
+        return
+
+    res = client.send_request('releases/%s' % edited, verb='GET', auth=True)
+
+    data = munch.unmunchify(res)
+
+    if 'errors' in data:
+        print_errors(data)
+
+    data['edited'] = edited
+    data['csrf_token'] = csrf
+
+    new_name = kwargs.pop('new_name')
+
+    if new_name is not None:
+        data['name'] = new_name
+
+    for k, v in kwargs.items():
+        if v is not None:
+            data[k] = v
+
+    save(client, **data)
+
+
+@releases.command(name='info')
+@handle_errors
+@click.argument('name')
+@url_option
+@staging_option
+def info_release(name, url, **kwargs):
+    """Retrieve and print info about a named release."""
+    client = bindings.BodhiClient(base_url=url, staging=kwargs['staging'])
+
+    res = client.send_request('releases/%s' % name, verb='GET', auth=False)
+
+    if 'errors' in res:
+        print_errors(res)
+
+    else:
+        print('Release:')
+        print_release(res)
+
+
+def save(client, **kwargs):
+    """
+    Save a new or edited release.
+
+    Args:
+        client (bodhi.client.bindings.BodhiClient): The Bodhi client to use for the request.
+        kwargs (dict): The parameters to send with the request.
+    """
+    res = client.send_request('releases/', verb='POST', auth=True,
+                              data=kwargs)
+
+    if 'errors' in res:
+        print_errors(res)
+
+    else:
+        print("Saved release:")
+        print_release(res)
+
+
+def print_release(release):
+    """
+    Print a given release to the terminal.
+
+    Args:
+        release (munch.Munch): The release to be printed.
+    """
+    print("  Name:                %s" % release['name'])
+    print("  Long Name:           %s" % release['long_name'])
+    print("  Version:             %s" % release['version'])
+    print("  Branch:              %s" % release['branch'])
+    print("  ID Prefix:           %s" % release['id_prefix'])
+    print("  Dist Tag:            %s" % release['dist_tag'])
+    print("  Stable Tag:          %s" % release['stable_tag'])
+    print("  Testing Tag:         %s" % release['testing_tag'])
+    print("  Candidate Tag:       %s" % release['candidate_tag'])
+    print("  Pending Signing Tag: %s" % release['pending_signing_tag'])
+    print("  Pending Testing Tag: %s" % release['pending_testing_tag'])
+    print("  Pending Stable Tag:  %s" % release['pending_stable_tag'])
+    print("  Override Tag:        %s" % release['override_tag'])
+    print("  State:               %s" % release['state'])
+
+
+def print_errors(data):
+    """
+    Print errors to the terminal and exit with code 1.
+
+    Args:
+        errors (munch.Munch): The errors to be formatted and printed.
+    """
+    for error in data['errors']:
+        print("ERROR: %s" % error['description'])
+
+    sys.exit(1)
+
+
 if __name__ == '__main__':
     cli()
diff --git a/bodhi/server/scripts/manage_releases.py b/bodhi/server/scripts/manage_releases.py
--- a/bodhi/server/scripts/manage_releases.py
+++ b/bodhi/server/scripts/manage_releases.py
@@ -27,183 +27,19 @@
     --pending-testing-tag f23-updates-testing-pending --override-tag f23-override --state pending
 """
 
-import sys
-import munch
-
 import click
 
-from bodhi.client import bindings
+from bodhi.client import cli
 
 
-@click.group()
 def main():
-    """Create and manage releases in Bodhi."""
-    pass  # pragma: no cover
-
-
-url_option = click.option(
-    '--url', envvar='BODHI_URL', default=bindings.BASE_URL,
-    help=('URL of a Bodhi server. Can be set with BODHI_URL environment variable'))
-
-
-@main.command()
-@click.option('--username')
-@click.option('--password', prompt=True, hide_input=True)
-@click.option('--name', help='Release name (eg: F20)')
-@click.option('--long-name', help='Long release name (eg: "Fedora 20")')
-@click.option('--id-prefix', help='Release prefix (eg: FEDORA)')
-@click.option('--version', help='Release version number (eg: 20)')
-@click.option('--branch', help='Git branch name (eg: f20)')
-@click.option('--dist-tag', help='Koji dist tag (eg: f20)')
-@click.option('--stable-tag', help='Koji stable tag (eg: f20-updates)')
-@click.option('--testing-tag',
-              help='Koji testing tag (eg: f20-updates-testing)')
-@click.option('--candidate-tag',
-              help='Koji candidate tag (eg: f20-updates-candidate)')
-@click.option('--pending-stable-tag',
-              help='Koji pending tag (eg: f20-updates-pending)')
-@click.option('--pending-testing-tag',
-              help='Koji pending testing tag (eg: f20-updates-testing-testing)')
-@click.option('--override-tag', help='Koji override tag (eg: f20-override)')
-@click.option('--state', type=click.Choice(['disabled', 'pending', 'current',
-                                            'archived']),
-              help='The state of the release')
-@url_option
-def create(username, password, url, **kwargs):
-    """Create a release."""
-    client = bindings.BodhiClient(base_url=url, username=username, password=password)
-    kwargs['csrf_token'] = client.csrf()
-
-    save(client, **kwargs)
-
-
-@main.command()
-@click.option('--username')
-@click.option('--password', prompt=True, hide_input=True)
-@click.option('--name', help='Release name (eg: F20)')
-@click.option('--new-name', help='New release name (eg: F20)')
-@click.option('--long-name', help='Long release name (eg: "Fedora 20")')
-@click.option('--id-prefix', help='Release prefix (eg: FEDORA)')
-@click.option('--version', help='Release version number (eg: 20)')
-@click.option('--branch', help='Git branch name (eg: f20)')
-@click.option('--dist-tag', help='Koji dist tag (eg: f20)')
-@click.option('--stable-tag', help='Koji stable tag (eg: f20-updates)')
-@click.option('--testing-tag',
-              help='Koji testing tag (eg: f20-updates-testing)')
-@click.option('--candidate-tag',
-              help='Koji candidate tag (eg: f20-updates-candidate)')
-@click.option('--pending-stable-tag',
-              help='Koji pending tag (eg: f20-updates-pending)')
-@click.option('--pending-testing-tag',
-              help='Koji pending testing tag (eg: f20-updates-testing-testing)')
-@click.option('--override-tag', help='Koji override tag (eg: f20-override)')
-@click.option('--state', type=click.Choice(['disabled', 'pending', 'current',
-                                            'archived']),
-              help='The state of the release')
-@url_option
-def edit(username, password, url, **kwargs):
-    """Edit an existing release."""
-    client = bindings.BodhiClient(base_url=url, username=username, password=password)
-    csrf = client.csrf()
-
-    edited = kwargs.pop('name')
-
-    if edited is None:
-        print("ERROR: Please specify the name of the release to edit")
-        return
-
-    res = client.send_request('releases/%s' % edited, verb='GET', auth=True)
-
-    data = munch.unmunchify(res)
-
-    if 'errors' in data:
-        print_errors(data)
-
-    data['edited'] = edited
-    data['csrf_token'] = csrf
-
-    new_name = kwargs.pop('new_name')
-
-    if new_name is not None:
-        data['name'] = new_name
-
-    for k, v in kwargs.items():
-        if v is not None:
-            data[k] = v
-
-    save(client, **data)
-
-
-@main.command()
-@click.argument('name')
-@url_option
-def info(name, url):
-    """Retrieve and print info about a named release."""
-    client = bindings.BodhiClient(base_url=url)
-
-    res = client.send_request('releases/%s' % name, verb='GET', auth=False)
-
-    if 'errors' in res:
-        print_errors(res)
-
-    else:
-        print('Release:')
-        print_release(res)
-
-
-def save(client, **kwargs):
     """
-    Save a new or edited release.
+    Create and manage releases in Bodhi.
 
-    Args:
-        client (bodhi.client.bindings.BodhiClient): The Bodhi client to use for the request.
-        kwargs (dict): The parameters to send with the request.
+    (This utility has been deprecated. Please use 'bodhi releases' instead.)
     """
-    res = client.send_request('releases/', verb='POST', auth=True,
-                              data=kwargs)
-
-    if 'errors' in res:
-        print_errors(res)
-
-    else:
-        print("Saved release:")
-        print_release(res)
-
-
-def print_release(release):
-    """
-    Print a given release to the terminal.
-
-    Args:
-        release (munch.Munch): The release to be printed.
-    """
-    print("  Name:                %s" % release['name'])
-    print("  Long Name:           %s" % release['long_name'])
-    print("  Version:             %s" % release['version'])
-    print("  Branch:              %s" % release['branch'])
-    print("  ID Prefix:           %s" % release['id_prefix'])
-    print("  Dist Tag:            %s" % release['dist_tag'])
-    print("  Stable Tag:          %s" % release['stable_tag'])
-    print("  Testing Tag:         %s" % release['testing_tag'])
-    print("  Candidate Tag:       %s" % release['candidate_tag'])
-    print("  Pending Signing Tag: %s" % release['pending_signing_tag'])
-    print("  Pending Testing Tag: %s" % release['pending_testing_tag'])
-    print("  Pending Stable Tag:  %s" % release['pending_stable_tag'])
-    print("  Override Tag:        %s" % release['override_tag'])
-    print("  State:               %s" % release['state'])
-
-
-def print_errors(data):
-    """
-    Print errors to the terminal and exit with code 1.
-
-    Args:
-        errors (munch.Munch): The errors to be formatted and printed.
-    """
-    for error in data['errors']:
-        print("ERROR: %s" % error['description'])
-
-    sys.exit(1)
+    click.echo("This utility has been deprecated. Please use 'bodhi releases' instead.")
+    cli.commands['releases']()
 
 
 if __name__ == '__main__':


Test patch:
diff --git a/bodhi/tests/client/__init__.py b/bodhi/tests/client/__init__.py
--- a/bodhi/tests/client/__init__.py
+++ b/bodhi/tests/client/__init__.py
@@ -866,4 +866,30 @@
   Expired: True
 """
 
+EXAMPLE_RELEASE_MUNCH = Munch({
+    u'dist_tag': u'f27', u'name': u'F27', u'testing_tag': u'f27-updates-testing',
+    u'pending_stable_tag': u'f27-updates-pending', u'pending_signing_tag': u'f27-signing-pending',
+    u'long_name': u'Fedora 27', u'state': u'pending', u'version': u'27',
+    u'override_tag': u'f27-override', u'branch': u'f27', u'id_prefix': u'FEDORA',
+    u'pending_testing_tag': u'f27-updates-testing-pending', u'stable_tag': u'f27-updates',
+    u'candidate_tag': u'f27-updates-candidate'})
+
+
+EXPECTED_RELEASE_OUTPUT = """Saved release:
+  Name:                F27
+  Long Name:           Fedora 27
+  Version:             27
+  Branch:              f27
+  ID Prefix:           FEDORA
+  Dist Tag:            f27
+  Stable Tag:          f27-updates
+  Testing Tag:         f27-updates-testing
+  Candidate Tag:       f27-updates-candidate
+  Pending Signing Tag: f27-signing-pending
+  Pending Testing Tag: f27-updates-testing-pending
+  Pending Stable Tag:  f27-updates-pending
+  Override Tag:        f27-override
+  State:               pending
+"""
+
 UNMATCHED_RESP = {"pants": "pants"}
diff --git a/bodhi/tests/client/test___init__.py b/bodhi/tests/client/test___init__.py
--- a/bodhi/tests/client/test___init__.py
+++ b/bodhi/tests/client/test___init__.py
@@ -1258,6 +1258,207 @@ def test_expired_override(self, send_request):
         self.assertEqual(bindings_client.base_url, 'http://localhost:6543/')
 
 
+class TestCreate(unittest.TestCase):
+    """
+    Test the create() function.
+    """
+    @mock.patch('bodhi.client.bindings.BodhiClient.csrf',
+                mock.MagicMock(return_value='a_csrf_token'))
+    @mock.patch('bodhi.client.bindings.BodhiClient.send_request',
+                return_value=client_test_data.EXAMPLE_RELEASE_MUNCH, autospec=True)
+    def test_url_flag(self, send_request):
+        """
+        Assert correct behavior with the --url flag.
+        """
+        runner = testing.CliRunner()
+
+        result = runner.invoke(
+            client.create_release,
+            ['--name', 'F27', '--url', 'http://localhost:6543', '--username', 'bowlofeggs',
+             '--password', 's3kr3t'])
+
+        self.assertEqual(result.exit_code, 0)
+        self.assertEqual(result.output, client_test_data.EXPECTED_RELEASE_OUTPUT)
+        bindings_client = send_request.mock_calls[0][1][0]
+        send_request.assert_called_once_with(
+            bindings_client, 'releases/', verb='POST', auth=True,
+            data={'dist_tag': None, 'csrf_token': 'a_csrf_token', 'staging': False, 'name': u'F27',
+                  'testing_tag': None, 'pending_stable_tag': None, 'long_name': None, 'state': None,
+                  'version': None, 'override_tag': None, 'branch': None, 'id_prefix': None,
+                  'pending_testing_tag': None, 'stable_tag': None, 'candidate_tag': None})
+        self.assertEqual(bindings_client.base_url, 'http://localhost:6543/')
+
+    @mock.patch('bodhi.client.bindings.BodhiClient.csrf',
+                mock.MagicMock(return_value='a_csrf_token'))
+    @mock.patch('bodhi.client.bindings.BodhiClient.send_request',
+                return_value={"errors": [{"description": "an error was encountered... :("}]},
+                autospec=True)
+    def test_create_with_errors(self, send_request):
+        """
+        Assert errors are printed if returned back in the request
+        """
+        runner = testing.CliRunner()
+
+        result = runner.invoke(
+            client.create_release,
+            ['--name', 'F27', '--url', 'http://localhost:6543', '--username', 'bowlofeggs',
+             '--password', 's3kr3t'])
+
+        self.assertEqual(result.exit_code, 1)
+        self.assertEqual(result.output, "ERROR: an error was encountered... :(\n")
+
+
+class TestEditRelease(unittest.TestCase):
+    """
+    Test the edit_release() function.
+    """
+    @mock.patch('bodhi.client.bindings.BodhiClient.csrf',
+                mock.MagicMock(return_value='a_csrf_token'))
+    @mock.patch('bodhi.client.bindings.BodhiClient.send_request',
+                return_value=client_test_data.EXAMPLE_RELEASE_MUNCH, autospec=True)
+    def test_url_flag(self, send_request):
+        """
+        Assert correct behavior with the --url flag.
+        """
+        runner = testing.CliRunner()
+
+        result = runner.invoke(
+            client.edit_release,
+            ['--name', 'F27', '--long-name', 'Fedora 27, the Greatest Fedora!', '--url',
+             'http://localhost:6543', '--username', 'bowlofeggs', '--password', 's3kr3t'])
+
+        self.assertEqual(result.exit_code, 0)
+        self.assertEqual(result.output, client_test_data.EXPECTED_RELEASE_OUTPUT)
+        bindings_client = send_request.mock_calls[0][1][0]
+        self.assertEqual(send_request.call_count, 2)
+        self.assertEqual(send_request.mock_calls[0],
+                         mock.call(bindings_client, 'releases/F27', verb='GET', auth=True))
+        self.assertEqual(
+            send_request.mock_calls[1],
+            mock.call(
+                bindings_client, 'releases/', verb='POST', auth=True,
+                data={'dist_tag': 'f27', 'csrf_token': 'a_csrf_token', 'staging': False,
+                      'name': 'F27', 'testing_tag': 'f27-updates-testing', 'edited': 'F27',
+                      'pending_stable_tag': 'f27-updates-pending',
+                      'pending_signing_tag': 'f27-signing-pending',
+                      'long_name': 'Fedora 27, the Greatest Fedora!', 'state': 'pending',
+                      'version': '27', 'override_tag': 'f27-override', 'branch': 'f27',
+                      'id_prefix': 'FEDORA', 'pending_testing_tag': 'f27-updates-testing-pending',
+                      'stable_tag': 'f27-updates', 'candidate_tag': 'f27-updates-candidate'}))
+        self.assertEqual(bindings_client.base_url, 'http://localhost:6543/')
+
+    @mock.patch('bodhi.client.bindings.BodhiClient.csrf',
+                mock.MagicMock(return_value='a_csrf_token'))
+    @mock.patch('bodhi.client.bindings.BodhiClient.send_request',
+                return_value=client_test_data.EXAMPLE_RELEASE_MUNCH, autospec=True)
+    def test_new_name_flag(self, send_request):
+        """
+        Assert correct behavior with the --new-name flag.
+        """
+        runner = testing.CliRunner()
+
+        result = runner.invoke(
+            client.edit_release,
+            ['--name', 'F27', '--new-name', 'fedora27', '--url',
+             'http://localhost:6543', '--username', 'bowlofeggs', '--password', 's3kr3t'])
+
+        self.assertEqual(result.exit_code, 0)
+        self.assertEqual(result.output, client_test_data.EXPECTED_RELEASE_OUTPUT)
+        bindings_client = send_request.mock_calls[0][1][0]
+        self.assertEqual(send_request.call_count, 2)
+        self.assertEqual(send_request.mock_calls[0],
+                         mock.call(bindings_client, 'releases/F27', verb='GET', auth=True))
+        self.assertEqual(
+            send_request.mock_calls[1],
+            mock.call(
+                bindings_client, 'releases/', verb='POST', auth=True,
+                data={'dist_tag': 'f27', 'csrf_token': 'a_csrf_token', 'staging': False,
+                      'name': 'fedora27', 'testing_tag': 'f27-updates-testing', 'edited': 'F27',
+                      'pending_stable_tag': 'f27-updates-pending',
+                      'pending_signing_tag': 'f27-signing-pending',
+                      'long_name': 'Fedora 27', 'state': 'pending',
+                      'version': '27', 'override_tag': 'f27-override', 'branch': 'f27',
+                      'id_prefix': 'FEDORA', 'pending_testing_tag': 'f27-updates-testing-pending',
+                      'stable_tag': 'f27-updates', 'candidate_tag': 'f27-updates-candidate'}))
+
+    @mock.patch('bodhi.client.bindings.BodhiClient.csrf',
+                mock.MagicMock(return_value='a_csrf_token'))
+    @mock.patch('bodhi.client.bindings.BodhiClient.send_request')
+    def test_edit_no_name_provided(self, send_request):
+        """
+        Assert we print an error and no request is sent if a --name is not provided.
+        """
+        runner = testing.CliRunner()
+
+        result = runner.invoke(
+            client.edit_release,
+            ['--long-name', 'Fedora 27, the Greatest Fedora!', '--url',
+             'http://localhost:6543', '--username', 'bowlofeggs', '--password', 's3kr3t'])
+
+        self.assertEqual(result.output, ("ERROR: Please specify the name of the release to edit\n"))
+        send_request.assert_not_called()
+
+    @mock.patch('bodhi.client.bindings.BodhiClient.csrf',
+                mock.MagicMock(return_value='a_csrf_token'))
+    @mock.patch('bodhi.client.bindings.BodhiClient.send_request',
+                return_value={"errors": [{"description": "an error was encountered... :("}]},
+                autospec=True)
+    def test_edit_with_errors(self, send_request):
+        """
+        Assert errors are printed if returned back in the request
+        """
+        runner = testing.CliRunner()
+
+        result = runner.invoke(
+            client.edit_release,
+            ['--name', 'F27', '--long-name', 'Fedora 27, the Greatest Fedora!', '--url',
+             'http://localhost:6543', '--username', 'bowlofeggs', '--password', 's3kr3t'])
+
+        self.assertEqual(result.exit_code, 1)
+        self.assertEqual(result.output, ("ERROR: an error was encountered... :(\n"))
+
+
+class TestInfo(unittest.TestCase):
+    """
+    Test the info() function.
+    """
+    @mock.patch('bodhi.client.bindings.BodhiClient.csrf',
+                mock.MagicMock(return_value='a_csrf_token'))
+    @mock.patch('bodhi.client.bindings.BodhiClient.send_request',
+                return_value=client_test_data.EXAMPLE_RELEASE_MUNCH, autospec=True)
+    def test_url_flag(self, send_request):
+        """
+        Assert correct behavior with the --url flag.
+        """
+        runner = testing.CliRunner()
+
+        result = runner.invoke(client.info_release, ['--url', 'http://localhost:6543', 'F27'])
+
+        self.assertEqual(result.exit_code, 0)
+        self.assertEqual(result.output,
+                         client_test_data.EXPECTED_RELEASE_OUTPUT.replace('Saved r', 'R'))
+        bindings_client = send_request.mock_calls[0][1][0]
+        send_request.assert_called_once_with(bindings_client, 'releases/F27', verb='GET',
+                                             auth=False)
+        self.assertEqual(bindings_client.base_url, 'http://localhost:6543/')
+
+    @mock.patch('bodhi.client.bindings.BodhiClient.csrf',
+                mock.MagicMock(return_value='a_csrf_token'))
+    @mock.patch('bodhi.client.bindings.BodhiClient.send_request',
+                return_value={"errors": [{"description": "an error was encountered... :("}]},
+                autospec=True)
+    def test_info_with_errors(self, send_request):
+        """
+        Assert errors are printed if returned back in the request
+        """
+        runner = testing.CliRunner()
+
+        result = runner.invoke(client.info_release, ['--url', 'http://localhost:6543', 'F27'])
+
+        self.assertEqual(result.exit_code, 1)
+        self.assertEqual(result.output, ("ERROR: an error was encountered... :(\n"))
+
+
 class TestHandleErrors(unittest.TestCase):
     """
     Test the handle_errors decorator
diff --git a/bodhi/tests/server/scripts/test_manage_releases.py b/bodhi/tests/server/scripts/test_manage_releases.py
--- a/bodhi/tests/server/scripts/test_manage_releases.py
+++ b/bodhi/tests/server/scripts/test_manage_releases.py
@@ -19,7 +19,6 @@
 """This module contains tests for bodhi.server.scripts.manage_releases."""
 import unittest
 
-from click import testing
 import mock
 import munch
 
@@ -53,201 +52,19 @@
 """
 
 
-class TestCreate(unittest.TestCase):
+class TestMain(unittest.TestCase):
     """
-    Test the create() function.
+    Test the main() function.
     """
-    @mock.patch('bodhi.client.bindings.BodhiClient.csrf',
-                mock.MagicMock(return_value='a_csrf_token'))
-    @mock.patch('bodhi.client.bindings.BodhiClient.send_request',
-                return_value=EXAMPLE_RELEASE_MUNCH, autospec=True)
-    def test_url_flag(self, send_request):
-        """
-        Assert correct behavior with the --url flag.
-        """
-        runner = testing.CliRunner()
-
-        result = runner.invoke(
-            manage_releases.create,
-            ['--name', 'F27', '--url', 'http://localhost:6543', '--username', 'bowlofeggs',
-             '--password', 's3kr3t'])
-
-        self.assertEqual(result.exit_code, 0)
-        self.assertEqual(result.output, EXPECTED_RELEASE_OUTPUT)
-        bindings_client = send_request.mock_calls[0][1][0]
-        send_request.assert_called_once_with(
-            bindings_client, 'releases/', verb='POST', auth=True,
-            data={'dist_tag': None, 'csrf_token': 'a_csrf_token', 'name': u'F27',
-                  'testing_tag': None, 'pending_stable_tag': None, 'long_name': None, 'state': None,
-                  'version': None, 'override_tag': None, 'branch': None, 'id_prefix': None,
-                  'pending_testing_tag': None, 'stable_tag': None, 'candidate_tag': None})
-        self.assertEqual(bindings_client.base_url, 'http://localhost:6543/')
-
-    @mock.patch('bodhi.client.bindings.BodhiClient.csrf',
-                mock.MagicMock(return_value='a_csrf_token'))
-    @mock.patch('bodhi.client.bindings.BodhiClient.send_request',
-                return_value={"errors": [{"description": "an error was encountered... :("}]},
-                autospec=True)
-    def test_create_with_errors(self, send_request):
-        """
-        Assert errors are printed if returned back in the request
-        """
-        runner = testing.CliRunner()
-
-        result = runner.invoke(
-            manage_releases.create,
-            ['--name', 'F27', '--url', 'http://localhost:6543', '--username', 'bowlofeggs',
-             '--password', 's3kr3t'])
-
-        self.assertEqual(result.exit_code, 1)
-        self.assertEqual(result.output, "ERROR: an error was encountered... :(\n")
-
-
-class TestEdit(unittest.TestCase):
-    """
-    Test the edit() function.
-    """
-    @mock.patch('bodhi.client.bindings.BodhiClient.csrf',
-                mock.MagicMock(return_value='a_csrf_token'))
-    @mock.patch('bodhi.client.bindings.BodhiClient.send_request',
-                return_value=EXAMPLE_RELEASE_MUNCH, autospec=True)
-    def test_url_flag(self, send_request):
-        """
-        Assert correct behavior with the --url flag.
-        """
-        runner = testing.CliRunner()
-
-        result = runner.invoke(
-            manage_releases.edit,
-            ['--name', 'F27', '--long-name', 'Fedora 27, the Greatest Fedora!', '--url',
-             'http://localhost:6543', '--username', 'bowlofeggs', '--password', 's3kr3t'])
-
-        self.assertEqual(result.exit_code, 0)
-        self.assertEqual(result.output, EXPECTED_RELEASE_OUTPUT)
-        bindings_client = send_request.mock_calls[0][1][0]
-        self.assertEqual(send_request.call_count, 2)
-        self.assertEqual(send_request.mock_calls[0],
-                         mock.call(bindings_client, 'releases/F27', verb='GET', auth=True))
-        self.assertEqual(
-            send_request.mock_calls[1],
-            mock.call(
-                bindings_client, 'releases/', verb='POST', auth=True,
-                data={'dist_tag': 'f27', 'csrf_token': 'a_csrf_token', 'name': 'F27',
-                      'testing_tag': 'f27-updates-testing', 'edited': 'F27',
-                      'pending_stable_tag': 'f27-updates-pending',
-                      'pending_signing_tag': 'f27-signing-pending',
-                      'long_name': 'Fedora 27, the Greatest Fedora!', 'state': 'pending',
-                      'version': '27', 'override_tag': 'f27-override', 'branch': 'f27',
-                      'id_prefix': 'FEDORA', 'pending_testing_tag': 'f27-updates-testing-pending',
-                      'stable_tag': 'f27-updates', 'candidate_tag': 'f27-updates-candidate'}))
-        self.assertEqual(bindings_client.base_url, 'http://localhost:6543/')
-
-    @mock.patch('bodhi.client.bindings.BodhiClient.csrf',
-                mock.MagicMock(return_value='a_csrf_token'))
-    @mock.patch('bodhi.client.bindings.BodhiClient.send_request',
-                return_value=EXAMPLE_RELEASE_MUNCH, autospec=True)
-    def test_new_name_flag(self, send_request):
-        """
-        Assert correct behavior with the --new-name flag.
-        """
-        runner = testing.CliRunner()
-
-        result = runner.invoke(
-            manage_releases.edit,
-            ['--name', 'F27', '--new-name', 'fedora27', '--url',
-             'http://localhost:6543', '--username', 'bowlofeggs', '--password', 's3kr3t'])
-
-        self.assertEqual(result.exit_code, 0)
-        self.assertEqual(result.output, EXPECTED_RELEASE_OUTPUT)
-        bindings_client = send_request.mock_calls[0][1][0]
-        self.assertEqual(send_request.call_count, 2)
-        self.assertEqual(send_request.mock_calls[0],
-                         mock.call(bindings_client, 'releases/F27', verb='GET', auth=True))
-        self.assertEqual(
-            send_request.mock_calls[1],
-            mock.call(
-                bindings_client, 'releases/', verb='POST', auth=True,
-                data={'dist_tag': 'f27', 'csrf_token': 'a_csrf_token', 'name': 'fedora27',
-                      'testing_tag': 'f27-updates-testing', 'edited': 'F27',
-                      'pending_stable_tag': 'f27-updates-pending',
-                      'pending_signing_tag': 'f27-signing-pending',
-                      'long_name': 'Fedora 27', 'state': 'pending',
-                      'version': '27', 'override_tag': 'f27-override', 'branch': 'f27',
-                      'id_prefix': 'FEDORA', 'pending_testing_tag': 'f27-updates-testing-pending',
-                      'stable_tag': 'f27-updates', 'candidate_tag': 'f27-updates-candidate'}))
-
-    @mock.patch('bodhi.client.bindings.BodhiClient.csrf',
-                mock.MagicMock(return_value='a_csrf_token'))
-    @mock.patch('bodhi.client.bindings.BodhiClient.send_request')
-    def test_edit_no_name_provided(self, send_request):
-        """
-        Assert we print an error and no request is sent if a --name is not provided.
-        """
-        runner = testing.CliRunner()
-
-        result = runner.invoke(
-            manage_releases.edit,
-            ['--long-name', 'Fedora 27, the Greatest Fedora!', '--url',
-             'http://localhost:6543', '--username', 'bowlofeggs', '--password', 's3kr3t'])
-
-        self.assertEqual(result.output, "ERROR: Please specify the name of the release to edit\n")
-        send_request.assert_not_called()
-
-    @mock.patch('bodhi.client.bindings.BodhiClient.csrf',
-                mock.MagicMock(return_value='a_csrf_token'))
-    @mock.patch('bodhi.client.bindings.BodhiClient.send_request',
-                return_value={"errors": [{"description": "an error was encountered... :("}]},
-                autospec=True)
-    def test_edit_with_errors(self, send_request):
-        """
-        Assert errors are printed if returned back in the request
-        """
-        runner = testing.CliRunner()
-
-        result = runner.invoke(
-            manage_releases.edit,
-            ['--name', 'F27', '--long-name', 'Fedora 27, the Greatest Fedora!', '--url',
-             'http://localhost:6543', '--username', 'bowlofeggs', '--password', 's3kr3t'])
-
-        self.assertEqual(result.exit_code, 1)
-        self.assertEqual(result.output, "ERROR: an error was encountered... :(\n")
-
-
-class TestInfo(unittest.TestCase):
-    """
-    Test the info() function.
-    """
-    @mock.patch('bodhi.client.bindings.BodhiClient.csrf',
-                mock.MagicMock(return_value='a_csrf_token'))
-    @mock.patch('bodhi.client.bindings.BodhiClient.send_request',
-                return_value=EXAMPLE_RELEASE_MUNCH, autospec=True)
-    def test_url_flag(self, send_request):
-        """
-        Assert correct behavior with the --url flag.
-        """
-        runner = testing.CliRunner()
-
-        result = runner.invoke(manage_releases.info, ['--url', 'http://localhost:6543', 'F27'])
-
-        self.assertEqual(result.exit_code, 0)
-        self.assertEqual(result.output, EXPECTED_RELEASE_OUTPUT.replace('Saved r', 'R'))
-        bindings_client = send_request.mock_calls[0][1][0]
-        send_request.assert_called_once_with(bindings_client, 'releases/F27', verb='GET',
-                                             auth=False)
-        self.assertEqual(bindings_client.base_url, 'http://localhost:6543/')
-
-    @mock.patch('bodhi.client.bindings.BodhiClient.csrf',
-                mock.MagicMock(return_value='a_csrf_token'))
-    @mock.patch('bodhi.client.bindings.BodhiClient.send_request',
-                return_value={"errors": [{"description": "an error was encountered... :("}]},
-                autospec=True)
-    def test_info_with_errors(self, send_request):
-        """
-        Assert errors are printed if returned back in the request
-        """
-        runner = testing.CliRunner()
-
-        result = runner.invoke(manage_releases.info, ['--url', 'http://localhost:6543', 'F27'])
-
-        self.assertEqual(result.exit_code, 1)
-        self.assertEqual(result.output, "ERROR: an error was encountered... :(\n")
+    @mock.patch('bodhi.client.releases')
+    def test_main(self, releases):
+        """
+        Assert correct behavior when main() is called.
+        """
+        try:
+            result = manage_releases.main()
+            self.assertEqual(result, ("This utility has been deprecated. ",
+                                      "Please use 'bodhi releases' instead."))
+            releases.assert_called_once_with()
+        except SystemExit:
+            pass