Repository: dbt-labs/dbt-core

Issue:
Have a variant of dbt --version for packages to help identify outdated packages
### Describe the feature
`dbt --version` notifies you when there's an updated version of dbt available to you. I'd love if there was an equivalent for packages (`dbt --version packages`?) which listed out each package (similar to #1236) but also checked the hub for updated versions. 

```
Up to date:
  - fishtown-analytics/audit_helper 
      installed version: 0.3.0

  - fivetran/fivetran_log
      installed version: 0.3.1

Updates available:
  - fishtown-analytics/dbt_utils: 
      installed version: 0.6.6 
      latest version:    0.7.0

Update your versions in packages.yml, then run dbt deps
```

### Additional context
- Not sure if/how this would work for packages loaded via git directly
- Not sure what should happen for packages installed via dependencies. E.g. 
    - Should having `calogica/dbt_expectations` mean that `calogica/dbt_date` is shown? My guess: maybe
    - Should it show there's an update available for `dbt_date` if it's only installed via dependency? My guess: probably not - that's for the dependency-creating package owner to deal with, and it's probably not helpful to make people wonder how to update something they can't control.

### Who will this benefit?
People who would like the latest and greatest from their packages, but don't want to manually go through the hub for their half-dozen different packages

### Are you interested in contributing this feature?
Sure, given ample pointers!



Discussion:
@joellabes I like it!

What if this were part of `dbt deps` output? Something like:
```bash
$ dbt deps
Running with dbt=0.19.2
Installing fishtown-analytics/dbt_utils@0.6.6
  Installed from version 0.6.6
  Latest version: 0.7.0
Installing fishtown-analytics/codegen@0.3.1
  Installed from version 0.3.1
  Latest version: 0.4.0
Installing fivetran/fivetran_utils@0.1.1
  Installed from version 0.1.1
  
Updates available for: dbt_utils, codegen
```

A few quick thoughts:
- This could only work for Hub-installed packages. I don't think it could work for git-installed packages. That's ok, it's one of the benefits of using the package registry.
- The input would have to be information from `packages.yml`, rather than the actual installed packages in `dbt_modules`. Packages don't record their version anywhere within their own project code—the version is the git tag in their repo. (I try to keep the `version` in `dbt_project.yml` up to date for Labs-maintained packages, but that's cosmetic only and not enforced.) So this command wouldn't be able to help with keeping `packages.yml` + `dbt_modules` in harmony, only `packages.yml` + the wider world.

If that version sounds reasonable to you, I think this could indeed be a good first issue
Yes I'm OK with all of that! Including it in deps output is much more discoverable.

In your example, is fivetran_utils' latest version not shown because it's up to date, or because 
>Packages don't record their version anywhere within their own project code

makes it unknowable?
I was thinking because it's the latest version. Could be:
```
Installing fivetran/fivetran_utils@0.1.1
  Installed from version 0.1.1
  Up to date!
```

Anything installed from git/local wouldn't be knowable.
Sweet! Let's do it
@joellabes Are you going to own building this feature for `dbt deps`? If not, I'd be happy to tackle this!
@sungchun12 I'm not likely to get to it in the next month or so, so if you want to jump in earlier then go for gold!
> @sungchun12 I'm not likely to get to it in the next month or so, so if you want to jump in earlier then go for gold!

Thanks a bunch for the baton pass 😄 . I'll officially own this and start this week!

Resolving patch:
diff --git a/core/dbt/deps/registry.py b/core/dbt/deps/registry.py
--- a/core/dbt/deps/registry.py
+++ b/core/dbt/deps/registry.py
@@ -30,9 +30,13 @@ def source_type(self) -> str:
 
 
 class RegistryPinnedPackage(RegistryPackageMixin, PinnedPackage):
-    def __init__(self, package: str, version: str) -> None:
+    def __init__(self,
+                 package: str,
+                 version: str,
+                 version_latest: str) -> None:
         super().__init__(package)
         self.version = version
+        self.version_latest = version_latest
 
     @property
     def name(self):
@@ -44,6 +48,9 @@ def source_type(self):
     def get_version(self):
         return self.version
 
+    def get_version_latest(self):
+        return self.version_latest
+
     def nice_version_name(self):
         return 'version {}'.format(self.version)
 
@@ -124,6 +131,7 @@ def resolved(self) -> RegistryPinnedPackage:
             available,
             self.install_prerelease
         )
+        available_latest = installable[-1]
 
         # for now, pick a version and then recurse. later on,
         # we'll probably want to traverse multiple options
@@ -132,4 +140,5 @@ def resolved(self) -> RegistryPinnedPackage:
         target = semver.resolve_to_specific_version(range_, installable)
         if not target:
             package_version_not_found(self.package, range_, installable)
-        return RegistryPinnedPackage(package=self.package, version=target)
+        return RegistryPinnedPackage(package=self.package, version=target,
+                                     version_latest=available_latest)
diff --git a/core/dbt/semver.py b/core/dbt/semver.py
--- a/core/dbt/semver.py
+++ b/core/dbt/semver.py
@@ -3,6 +3,7 @@
 from typing import List
 
 from packaging import version as packaging_version
+from distutils.version import StrictVersion
 
 from dbt.exceptions import VersionsNotCompatibleException
 import dbt.utils
@@ -437,11 +438,10 @@ def filter_installable(
         versions: List[str],
         install_prerelease: bool
 ) -> List[str]:
-    if install_prerelease:
-        return versions
     installable = []
     for version_string in versions:
         version = VersionSpecifier.from_version_string(version_string)
-        if not version.prerelease:
+        if install_prerelease or not version.prerelease:
             installable.append(version_string)
+    installable.sort(key=StrictVersion)
     return installable
diff --git a/core/dbt/task/deps.py b/core/dbt/task/deps.py
--- a/core/dbt/task/deps.py
+++ b/core/dbt/task/deps.py
@@ -56,19 +56,36 @@ def run(self):
                 self.config, self.config.cli_vars
             ))
 
+            packages_to_upgrade = []
             for package in final_deps:
+                package_name = package.name
+                source_type = package.source_type()
+                version = package.get_version()
+
                 logger.info('Installing {}', package)
                 package.install(self.config, renderer)
                 logger.info('  Installed from {}',
                             package.nice_version_name())
+                if source_type == 'hub':
+                    version_latest = package.get_version_latest()
+                    if version_latest != version:
+                        packages_to_upgrade.append(package_name)
+                        logger.info('  Updated version available: {}',
+                                    version_latest)
+                    else:
+                        logger.info('  Up to date!')
                 if package.get_subdirectory():
-                    logger.info('   and subdirectory {}\n',
+                    logger.info('   and subdirectory {}',
                                 package.get_subdirectory())
 
                 self.track_package_install(
-                    package_name=package.name,
-                    source_type=package.source_type(),
-                    version=package.get_version())
+                    package_name=package_name,
+                    source_type=source_type,
+                    version=version)
+            if packages_to_upgrade:
+                logger.info('\nUpdates available for packages: {} \
+                \nUpdate your versions in packages.yml, then run dbt deps',
+                            packages_to_upgrade)
 
     @classmethod
     def from_args(cls, args):


Test patch:
diff --git a/test/unit/test_deps.py b/test/unit/test_deps.py
--- a/test/unit/test_deps.py
+++ b/test/unit/test_deps.py
@@ -386,6 +386,91 @@ def test_resolve_ranges_install_prerelease_true(self):
         self.assertEqual(c_pinned.version, '0.1.4a1')
         self.assertEqual(c_pinned.source_type(), 'hub')
 
+    def test_get_version_latest_prelease_true(self):
+        a_contract = RegistryPackage(
+            package='dbt-labs-test/a',
+            version='>0.1.0',
+            install_prerelease=True
+        )
+        b_contract = RegistryPackage(
+            package='dbt-labs-test/a',
+            version='<0.1.4'
+        )
+        a = RegistryUnpinnedPackage.from_contract(a_contract)
+        b = RegistryUnpinnedPackage.from_contract(b_contract)
+        c = a.incorporate(b)
+
+        self.assertEqual(c.package, 'dbt-labs-test/a')
+        self.assertEqual(
+            c.versions,
+            [
+                VersionSpecifier(
+                    build=None,
+                    major='0',
+                    matcher='>',
+                    minor='1',
+                    patch='0',
+                    prerelease=None,
+                ),
+                VersionSpecifier(
+                    build=None,
+                    major='0',
+                    matcher='<',
+                    minor='1',
+                    patch='4',
+                    prerelease=None,
+                ),
+            ]
+        )
+
+        c_pinned = c.resolved()
+        self.assertEqual(c_pinned.package, 'dbt-labs-test/a')
+        self.assertEqual(c_pinned.version, '0.1.3')
+        self.assertEqual(c_pinned.get_version_latest(), '0.1.4a1')
+        self.assertEqual(c_pinned.source_type(), 'hub')
+
+    def test_get_version_latest_prelease_false(self):
+        a_contract = RegistryPackage(
+            package='dbt-labs-test/a',
+            version='>0.1.0',
+            install_prerelease=False
+        )
+        b_contract = RegistryPackage(
+            package='dbt-labs-test/a',
+            version='<0.1.4'
+        )
+        a = RegistryUnpinnedPackage.from_contract(a_contract)
+        b = RegistryUnpinnedPackage.from_contract(b_contract)
+        c = a.incorporate(b)
+
+        self.assertEqual(c.package, 'dbt-labs-test/a')
+        self.assertEqual(
+            c.versions,
+            [
+                VersionSpecifier(
+                    build=None,
+                    major='0',
+                    matcher='>',
+                    minor='1',
+                    patch='0',
+                    prerelease=None,
+                ),
+                VersionSpecifier(
+                    build=None,
+                    major='0',
+                    matcher='<',
+                    minor='1',
+                    patch='4',
+                    prerelease=None,
+                ),
+            ]
+        )
+
+        c_pinned = c.resolved()
+        self.assertEqual(c_pinned.package, 'dbt-labs-test/a')
+        self.assertEqual(c_pinned.version, '0.1.3')
+        self.assertEqual(c_pinned.get_version_latest(), '0.1.3')
+        self.assertEqual(c_pinned.source_type(), 'hub')
 
 class MockRegistry:
     def __init__(self, packages):
diff --git a/test/unit/test_semver.py b/test/unit/test_semver.py
--- a/test/unit/test_semver.py
+++ b/test/unit/test_semver.py
@@ -175,11 +175,11 @@ def test__resolve_to_specific_version(self):
 
     def test__filter_installable(self):
         assert filter_installable(
-            ['1.0.0', '1.1.0', '1.2.0a1'],
+            ['1.1.0',  '1.2.0a1', '1.0.0'],
             install_prerelease=True
         ) == ['1.0.0', '1.1.0', '1.2.0a1']
 
         assert filter_installable(
-            ['1.0.0', '1.1.0', '1.2.0a1'],
+            ['1.1.0',  '1.2.0a1', '1.0.0'],
             install_prerelease=False
         ) == ['1.0.0', '1.1.0']