Repository: dbt-labs/dbt-core

Issue:
[CT-229] Adapter compatible message rule update
For adapter compatible message, there are two rules we missed in the previous commit:
- plugins can put out patch releases before dbt-core puts out a patch release, so `dbt-core==1.0.1` and `dbt-synapse==1.0.2` can be both available
- `dbt-core` + plugins are compatible at the minor-version level.

Here's the [original thread](https://github.com/dbt-labs/dbt-core/pull/4565#pullrequestreview-872805134), #4565 


Discussion:
Two other quick thoughts:
- Users may have prerelease versions of plugins installed, in which case we should say `Ahead of the latest release!` rather than `Update available!`
- If one or more core/plugins is out of date, batch the upgrade instruction once at the end: `You can find instructions for upgrading here: https://docs.getdbt.com/dbt-cli/install/overview`
I think we should also take another swing at the specific language, so it reads a bit more consistently. Something like:
```
$ dbt --version
Core:
  installed: 1.0.3
  latest:    1.0.4 - New [major/minor/patch] version available!

Plugins:
  - snowflake:  1.0.0 - Up to date
  - databricks: 1.0.0 - Update available! [note: newer patches only]
  - synapse:    0.21.0 - Not compatible!

Your version of dbt is out of date! You can find instructions for upgrading here:
https://docs.getdbt.com/docs/installation
```
>`   - databricks: 1.0.0 - Update available! [note: newer patches only]`

This might be trickier than I initially thought. As I understand it, the PyPi API only makes available "latest" and "all versions." There's no way to say "give me latest patch for minor version 1.0," except by churning through the JSON response of "all versions."

Resolving patch:
diff --git a/core/dbt/version.py b/core/dbt/version.py
--- a/core/dbt/version.py
+++ b/core/dbt/version.py
@@ -3,7 +3,7 @@
 import os
 import glob
 import json
-from typing import Iterator
+from typing import Iterator, List, Optional, Tuple
 
 import requests
 
@@ -16,7 +16,34 @@
 PYPI_VERSION_URL = "https://pypi.org/pypi/dbt-core/json"
 
 
-def get_latest_version(version_url: str = PYPI_VERSION_URL):
+def get_version_information() -> str:
+    flags.USE_COLORS = True if not flags.USE_COLORS else None
+
+    installed = get_installed_version()
+    latest = get_latest_version()
+
+    core_msg_lines, core_info_msg = _get_core_msg_lines(installed, latest)
+    core_msg = _format_core_msg(core_msg_lines)
+    plugin_version_msg = _get_plugins_msg(installed)
+
+    msg_lines = [core_msg]
+
+    if core_info_msg != "":
+        msg_lines.append(core_info_msg)
+
+    msg_lines.append(plugin_version_msg)
+    msg_lines.append("")
+
+    return "\n\n".join(msg_lines)
+
+
+def get_installed_version() -> dbt.semver.VersionSpecifier:
+    return dbt.semver.VersionSpecifier.from_version_string(__version__)
+
+
+def get_latest_version(
+    version_url: str = PYPI_VERSION_URL,
+) -> Optional[dbt.semver.VersionSpecifier]:
     try:
         resp = requests.get(version_url)
         data = resp.json()
@@ -27,81 +54,168 @@ def get_latest_version(version_url: str = PYPI_VERSION_URL):
     return dbt.semver.VersionSpecifier.from_version_string(version_string)
 
 
-def get_installed_version():
-    return dbt.semver.VersionSpecifier.from_version_string(__version__)
+def _get_core_msg_lines(installed, latest) -> Tuple[List[List[str]], str]:
+    installed_s = installed.to_version_string(skip_matcher=True)
+    installed_line = ["installed", installed_s, ""]
+    update_info = ""
 
+    if latest is None:
+        update_info = (
+            "  The latest version of dbt-core could not be determined!\n"
+            "  Make sure that the following URL is accessible:\n"
+            f"  {PYPI_VERSION_URL}"
+        )
+        return [installed_line], update_info
+
+    latest_s = latest.to_version_string(skip_matcher=True)
+    latest_line = ["latest", latest_s, green("Up to date!")]
+
+    if installed > latest:
+        latest_line[2] = green("Ahead of latest version!")
+    elif installed < latest:
+        latest_line[2] = yellow("Update available!")
+        update_info = (
+            "  Your version of dbt-core is out of date!\n"
+            "  You can find instructions for upgrading here:\n"
+            "  https://docs.getdbt.com/docs/installation"
+        )
 
-def get_package_pypi_url(package_name: str) -> str:
-    return f"https://pypi.org/pypi/dbt-{package_name}/json"
+    return [
+        installed_line,
+        latest_line,
+    ], update_info
 
 
-def get_version_information():
-    flags.USE_COLORS = True if not flags.USE_COLORS else None
+def _format_core_msg(lines: List[List[str]]) -> str:
+    msg = "Core:\n"
+    msg_lines = []
 
-    installed = get_installed_version()
-    latest = get_latest_version()
+    for name, version, update_msg in _pad_lines(lines, seperator=":"):
+        line_msg = f"  - {name} {version}"
+        if update_msg != "":
+            line_msg += f" - {update_msg}"
+        msg_lines.append(line_msg)
 
-    installed_s = installed.to_version_string(skip_matcher=True)
-    if latest is None:
-        latest_s = "unknown"
-    else:
-        latest_s = latest.to_version_string(skip_matcher=True)
-
-    version_msg = "installed version: {}\n" "   latest version: {}\n\n".format(
-        installed_s, latest_s
-    )
-
-    plugin_version_msg = "Plugins:\n"
-    for plugin_name, version in _get_dbt_plugins_info():
-        plugin_version = dbt.semver.VersionSpecifier.from_version_string(version)
-        latest_plugin_version = get_latest_version(version_url=get_package_pypi_url(plugin_name))
-        plugin_update_msg = ""
-        if installed == plugin_version or (
-            latest_plugin_version and plugin_version == latest_plugin_version
-        ):
-            compatibility_msg = green("Up to date!")
-        else:
-            if latest_plugin_version:
-                if installed.major == plugin_version.major:
-                    compatibility_msg = yellow("Update available!")
-                else:
-                    compatibility_msg = red("Out of date!")
-                plugin_update_msg = (
-                    "  Your version of dbt-{} is out of date! "
-                    "You can find instructions for upgrading here:\n"
-                    "  https://docs.getdbt.com/dbt-cli/install/overview\n\n"
-                ).format(plugin_name)
-            else:
-                compatibility_msg = yellow("No PYPI version available")
-
-        plugin_version_msg += ("  - {}: {} - {}\n" "{}").format(
-            plugin_name, version, compatibility_msg, plugin_update_msg
-        )
+    return msg + "\n".join(msg_lines)
 
-    if latest is None:
-        return (
-            "{}The latest version of dbt could not be determined!\n"
-            "Make sure that the following URL is accessible:\n{}\n\n{}".format(
-                version_msg, PYPI_VERSION_URL, plugin_version_msg
-            )
-        )
 
-    if installed == latest:
-        return f"{version_msg}{green('Up to date!')}\n\n{plugin_version_msg}"
+def _get_plugins_msg(installed: dbt.semver.VersionSpecifier) -> str:
+    msg_lines = ["Plugins:"]
 
-    elif installed > latest:
-        return "{}Your version of dbt is ahead of the latest " "release!\n\n{}".format(
-            version_msg, plugin_version_msg
+    plugins = []
+    display_update_msg = False
+    for name, version_s in _get_dbt_plugins_info():
+        compatability_msg, needs_update = _get_plugin_msg_info(name, version_s, installed)
+        if needs_update:
+            display_update_msg = True
+        plugins.append([name, version_s, compatability_msg])
+
+    for plugin in _pad_lines(plugins, seperator=":"):
+        msg_lines.append(_format_single_plugin(plugin, ""))
+
+    if display_update_msg:
+        update_msg = (
+            "  At least one plugin is out of date or incompatible with dbt-core.\n"
+            "  You can find instructions for upgrading here:\n"
+            "  https://docs.getdbt.com/docs/installation"
         )
+        msg_lines += ["", update_msg]
+
+    return "\n".join(msg_lines)
+
+
+def _get_plugin_msg_info(
+    name: str, version_s: str, core: dbt.semver.VersionSpecifier
+) -> Tuple[str, bool]:
+    plugin = dbt.semver.VersionSpecifier.from_version_string(version_s)
+    latest_plugin = get_latest_version(version_url=get_package_pypi_url(name))
 
+    needs_update = False
+
+    if plugin.major != core.major or plugin.minor != core.minor:
+        compatibility_msg = red("Not compatible!")
+        needs_update = True
+        return (compatibility_msg, needs_update)
+
+    if not latest_plugin:
+        compatibility_msg = yellow("Could not determine latest version")
+        return (compatibility_msg, needs_update)
+
+    if plugin < latest_plugin:
+        compatibility_msg = yellow("Update available!")
+        needs_update = True
+    elif plugin > latest_plugin:
+        compatibility_msg = green("Ahead of latest version!")
     else:
-        return (
-            "{}Your version of dbt is out of date! "
-            "You can find instructions for upgrading here:\n"
-            "https://docs.getdbt.com/docs/installation\n\n{}".format(
-                version_msg, plugin_version_msg
-            )
-        )
+        compatibility_msg = green("Up to date!")
+
+    return (compatibility_msg, needs_update)
+
+
+def _format_single_plugin(plugin: List[str], update_msg: str) -> str:
+    name, version_s, compatability_msg = plugin
+    msg = f"  - {name} {version_s} - {compatability_msg}"
+    if update_msg != "":
+        msg += f"\n{update_msg}\n"
+    return msg
+
+
+def _pad_lines(lines: List[List[str]], seperator: str = "") -> List[List[str]]:
+    if len(lines) == 0:
+        return []
+
+    # count the max line length for each column in the line
+    counter = [0] * len(lines[0])
+    for line in lines:
+        for i, item in enumerate(line):
+            counter[i] = max(counter[i], len(item))
+
+    result: List[List[str]] = []
+    for i, line in enumerate(lines):
+
+        # add another list to hold padded strings
+        if len(result) == i:
+            result.append([""] * len(line))
+
+        # iterate over columns in the line
+        for j, item in enumerate(line):
+
+            # the last column does not need padding
+            if j == len(line) - 1:
+                result[i][j] = item
+                continue
+
+            # if the following column has no length
+            # the string does not need padding
+            if counter[j + 1] == 0:
+                result[i][j] = item
+                continue
+
+            # only add the seperator to the first column
+            offset = 0
+            if j == 0 and seperator != "":
+                item += seperator
+                offset = len(seperator)
+
+            result[i][j] = item.ljust(counter[j] + offset)
+
+    return result
+
+
+def get_package_pypi_url(package_name: str) -> str:
+    return f"https://pypi.org/pypi/dbt-{package_name}/json"
+
+
+def _get_dbt_plugins_info() -> Iterator[Tuple[str, str]]:
+    for plugin_name in _get_adapter_plugin_names():
+        if plugin_name == "core":
+            continue
+        try:
+            mod = importlib.import_module(f"dbt.adapters.{plugin_name}.__version__")
+        except ImportError:
+            # not an adapter
+            continue
+        yield plugin_name, mod.version  # type: ignore
 
 
 def _get_adapter_plugin_names() -> Iterator[str]:
@@ -120,17 +234,5 @@ def _get_adapter_plugin_names() -> Iterator[str]:
             yield plugin_name
 
 
-def _get_dbt_plugins_info():
-    for plugin_name in _get_adapter_plugin_names():
-        if plugin_name == "core":
-            continue
-        try:
-            mod = importlib.import_module(f"dbt.adapters.{plugin_name}.__version__")
-        except ImportError:
-            # not an adapter
-            continue
-        yield plugin_name, mod.version
-
-
 __version__ = "1.1.0b1"
 installed = get_installed_version()


Test patch:
diff --git a/tests/unit/test_version.py b/tests/unit/test_version.py
--- a/tests/unit/test_version.py
+++ b/tests/unit/test_version.py
@@ -16,14 +16,14 @@ def test_all_versions_equal(self, mocker):
         actual = dbt.version.get_version_information()
         expected = "\n".join(
             [
-                "installed version: 1.0.0",
-                "   latest version: 1.0.0",
-                "",
-                green("Up to date!"),
+                "Core:",
+                "  - installed: 1.0.0",
+                f"  - latest:    1.0.0 - {green('Up to date!')}",
                 "",
                 "Plugins:",
                 f"  - foobar: 1.0.0 - {green('Up to date!')}",
                 "",
+                "",
             ]
         )
 
@@ -42,14 +42,14 @@ def test_core_ahead(self, mocker):
         actual = dbt.version.get_version_information()
         expected = "\n".join(
             [
-                "installed version: 1.0.1",
-                "   latest version: 1.0.0",
-                "",
-                "Your version of dbt is ahead of the latest release!",
+                "Core:",
+                "  - installed: 1.0.1",
+                f"  - latest:    1.0.0 - {yellow('Ahead of latest version!')}",
                 "",
                 "Plugins:",
                 f"  - foobar: 1.0.0 - {green('Up to date!')}",
                 "",
+                "",
             ]
         )
 
@@ -68,15 +68,18 @@ def test_core_behind(self, mocker):
         actual = dbt.version.get_version_information()
         expected = "\n".join(
             [
-                "installed version: 1.0.0",
-                "   latest version: 1.0.1",
+                "Core:",
+                "  - installed: 1.0.0",
+                f"  - latest:    1.0.1 - {yellow('Update available!')}",
                 "",
-                "Your version of dbt is out of date! You can find instructions for upgrading here:",
-                "https://docs.getdbt.com/docs/installation",
+                "  Your version of dbt-core is out of date!",
+                "  You can find instructions for upgrading here:",
+                "  https://docs.getdbt.com/docs/installation",
                 "",
                 "Plugins:",
                 f"  - foobar: 1.0.0 - {green('Up to date!')}",
                 "",
+                "",
             ]
         )
 
@@ -94,16 +97,17 @@ def test_core_no_latest(self, mocker):
         actual = dbt.version.get_version_information()
         expected = "\n".join(
             [
-                "installed version: 1.0.0",
-                "   latest version: unknown",
+                "Core:",
+                "  - installed: 1.0.0",
                 "",
-                "The latest version of dbt could not be determined!",
-                "Make sure that the following URL is accessible:",
-                "https://pypi.org/pypi/dbt-core/json",
+                "  The latest version of dbt-core could not be determined!",
+                "  Make sure that the following URL is accessible:",
+                "  https://pypi.org/pypi/dbt-core/json",
                 "",
                 "Plugins:",
                 f"  - foobar: 1.0.0 - {green('Up to date!')}",
                 "",
+                "",
             ]
         )
 
@@ -119,13 +123,13 @@ def test_plugins_none(self, mocker):
         actual = dbt.version.get_version_information()
         expected = "\n".join(
             [
-                "installed version: 1.0.0",
-                "   latest version: 1.0.0",
-                "",
-                green("Up to date!"),
+                "Core:",
+                "  - installed: 1.0.0",
+                f"  - latest:    1.0.0 - {green('Up to date!')}",
                 "",
                 "Plugins:",
                 "",
+                "",
             ]
         )
 
@@ -147,17 +151,17 @@ def test_plugins_multiple(self, mocker):
         actual = dbt.version.get_version_information()
         expected = "\n".join(
             [
-                "installed version: 1.0.0",
-                "   latest version: 1.0.0",
-                "",
-                green("Up to date!"),
+                "Core:",
+                "  - installed: 1.0.0",
+                f"  - latest:    1.0.0 - {green('Up to date!')}",
                 "",
                 "Plugins:",
-                f"  - foobar: 1.0.0 - {green('Up to date!')}",
-                f"  - bazqux: 1.0.0 - {green('Up to date!')}",
-                f"  - quuxcorge: 1.0.0 - {green('Up to date!')}",
+                f"  - foobar:       1.0.0 - {green('Up to date!')}",
+                f"  - bazqux:       1.0.0 - {green('Up to date!')}",
+                f"  - quuxcorge:    1.0.0 - {green('Up to date!')}",
                 f"  - graultgarply: 1.0.0 - {green('Up to date!')}",
                 "",
+                "",
             ]
         )
 
@@ -176,14 +180,14 @@ def test_plugin_match_core_match_latest(self, mocker):
         actual = dbt.version.get_version_information()
         expected = "\n".join(
             [
-                "installed version: 1.0.0",
-                "   latest version: 1.0.0",
-                "",
-                green("Up to date!"),
+                "Core:",
+                "  - installed: 1.0.0",
+                f"  - latest:    1.0.0 - {green('Up to date!')}",
                 "",
                 "Plugins:",
                 f"  - foobar: 1.0.0 - {green('Up to date!')}",
                 "",
+                "",
             ]
         )
 
@@ -202,13 +206,13 @@ def test_plugin_match_core_no_latest(self, mocker):
         actual = dbt.version.get_version_information()
         expected = "\n".join(
             [
-                "installed version: 1.0.0",
-                "   latest version: 1.0.0",
-                "",
-                green("Up to date!"),
+                "Core:",
+                "  - installed: 1.0.0",
+                f"  - latest:    1.0.0 - {green('Up to date!')}",
                 "",
                 "Plugins:",
-                f"  - foobar: 1.0.0 - {green('Up to date!')}",
+                f"  - foobar: 1.0.0 - {yellow('Could not determine latest version')}",
+                "",
                 "",
             ]
         )
@@ -228,13 +232,17 @@ def test_plugin_match_core_behind_latest(self, mocker):
         actual = dbt.version.get_version_information()
         expected = "\n".join(
             [
-                "installed version: 1.0.0",
-                "   latest version: 1.0.0",
-                "",
-                green("Up to date!"),
+                "Core:",
+                "  - installed: 1.0.0",
+                f"  - latest:    1.0.0 - {green('Up to date!')}",
                 "",
                 "Plugins:",
-                f"  - foobar: 1.0.0 - {green('Up to date!')}",
+                f"  - foobar: 1.0.0 - {yellow('Update available!')}",
+                "",
+                "  At least one plugin is out of date or incompatible with dbt-core.",
+                "  You can find instructions for upgrading here:",
+                "  https://docs.getdbt.com/docs/installation",
+                "",
                 "",
             ]
         )
@@ -254,13 +262,17 @@ def test_plugin_match_core_ahead_latest(self, mocker):
         actual = dbt.version.get_version_information()
         expected = "\n".join(
             [
-                "installed version: 1.0.0",
-                "   latest version: 1.0.0",
-                "",
-                green("Up to date!"),
+                "Core:",
+                "  - installed: 1.0.0",
+                f"  - latest:    1.0.0 - {green('Up to date!')}",
                 "",
                 "Plugins:",
-                f"  - foobar: 1.0.0 - {green('Up to date!')}",
+                f"  - foobar: 1.0.0 - {yellow('Update available!')}",
+                "",
+                "  At least one plugin is out of date or incompatible with dbt-core.",
+                "  You can find instructions for upgrading here:",
+                "  https://docs.getdbt.com/docs/installation",
+                "",
                 "",
             ]
         )
@@ -280,13 +292,17 @@ def test_plugin_diff_core_major_match_latest(self, mocker):
         actual = dbt.version.get_version_information()
         expected = "\n".join(
             [
-                "installed version: 2.0.0",
-                "   latest version: 2.0.0",
-                "",
-                green("Up to date!"),
+                "Core:",
+                "  - installed: 2.0.0",
+                f"  - latest:    2.0.0 - {green('Up to date!')}",
                 "",
                 "Plugins:",
-                f"  - foobar: 1.0.0 - {green('Up to date!')}",
+                f"  - foobar: 1.0.0 - {red('Not compatible!')}",
+                "",
+                "  At least one plugin is out of date or incompatible with dbt-core.",
+                "  You can find instructions for upgrading here:",
+                "  https://docs.getdbt.com/docs/installation",
+                "",
                 "",
             ]
         )
@@ -306,13 +322,17 @@ def test_plugin_diff_core_major_no_latest(self, mocker):
         actual = dbt.version.get_version_information()
         expected = "\n".join(
             [
-                "installed version: 2.0.0",
-                "   latest version: 2.0.0",
-                "",
-                green("Up to date!"),
+                "Core:",
+                "  - installed: 2.0.0",
+                f"  - latest:    2.0.0 - {green('Up to date!')}",
                 "",
                 "Plugins:",
-                f"  - foobar: 1.0.0 - {yellow('No PYPI version available')}",
+                f"  - foobar: 1.0.0 - {red('Not compatible!')}",
+                "",
+                "  At least one plugin is out of date or incompatible with dbt-core.",
+                "  You can find instructions for upgrading here:",
+                "  https://docs.getdbt.com/docs/installation",
+                "",
                 "",
             ]
         )
@@ -332,15 +352,16 @@ def test_plugin_diff_core_major_ahead_latest(self, mocker):
         actual = dbt.version.get_version_information()
         expected = "\n".join(
             [
-                "installed version: 2.0.0",
-                "   latest version: 2.0.0",
-                "",
-                green("Up to date!"),
+                "Core:",
+                "  - installed: 2.0.0",
+                f"  - latest:    2.0.0 - {green('Up to date!')}",
                 "",
                 "Plugins:",
-                f"  - foobar: 1.0.0 - {red('Out of date!')}",
-                "  Your version of dbt-foobar is out of date! You can find instructions for upgrading here:",
-                "  https://docs.getdbt.com/dbt-cli/install/overview",
+                f"  - foobar: 1.0.0 - {red('Not compatible!')}",
+                "",
+                "  At least one plugin is out of date or incompatible with dbt-core.",
+                "  You can find instructions for upgrading here:",
+                "  https://docs.getdbt.com/docs/installation",
                 "",
                 "",
             ]
@@ -361,15 +382,16 @@ def test_plugin_diff_core_major_behind_latest(self, mocker):
         actual = dbt.version.get_version_information()
         expected = "\n".join(
             [
-                "installed version: 2.0.0",
-                "   latest version: 2.0.0",
-                "",
-                green("Up to date!"),
+                "Core:",
+                "  - installed: 2.0.0",
+                f"  - latest:    2.0.0 - {green('Up to date!')}",
                 "",
                 "Plugins:",
-                f"  - foobar: 1.0.0 - {red('Out of date!')}",
-                "  Your version of dbt-foobar is out of date! You can find instructions for upgrading here:",
-                "  https://docs.getdbt.com/dbt-cli/install/overview",
+                f"  - foobar: 1.0.0 - {red('Not compatible!')}",
+                "",
+                "  At least one plugin is out of date or incompatible with dbt-core.",
+                "  You can find instructions for upgrading here:",
+                "  https://docs.getdbt.com/docs/installation",
                 "",
                 "",
             ]
@@ -390,13 +412,17 @@ def test_plugin_diff_core_minor_match_latest(self, mocker):
         actual = dbt.version.get_version_information()
         expected = "\n".join(
             [
-                "installed version: 1.1.0",
-                "   latest version: 1.1.0",
-                "",
-                green("Up to date!"),
+                "Core:",
+                "  - installed: 1.1.0",
+                f"  - latest:    1.1.0 - {green('Up to date!')}",
                 "",
                 "Plugins:",
-                f"  - foobar: 1.0.0 - {green('Up to date!')}",
+                f"  - foobar: 1.0.0 - {red('Not compatible!')}",
+                "",
+                "  At least one plugin is out of date or incompatible with dbt-core.",
+                "  You can find instructions for upgrading here:",
+                "  https://docs.getdbt.com/docs/installation",
+                "",
                 "",
             ]
         )
@@ -416,13 +442,17 @@ def test_plugin_diff_core_minor_no_latest(self, mocker):
         actual = dbt.version.get_version_information()
         expected = "\n".join(
             [
-                "installed version: 1.1.0",
-                "   latest version: 1.1.0",
-                "",
-                green("Up to date!"),
+                "Core:",
+                "  - installed: 1.1.0",
+                f"  - latest:    1.1.0 - {green('Up to date!')}",
                 "",
                 "Plugins:",
-                f"  - foobar: 1.0.0 - {yellow('No PYPI version available')}",
+                f"  - foobar: 1.0.0 - {red('Not compatible!')}",
+                "",
+                "  At least one plugin is out of date or incompatible with dbt-core.",
+                "  You can find instructions for upgrading here:",
+                "  https://docs.getdbt.com/docs/installation",
+                "",
                 "",
             ]
         )
@@ -442,15 +472,16 @@ def test_plugin_diff_core_minor_ahead_latest(self, mocker):
         actual = dbt.version.get_version_information()
         expected = "\n".join(
             [
-                "installed version: 1.1.0",
-                "   latest version: 1.1.0",
-                "",
-                green("Up to date!"),
+                "Core:",
+                "  - installed: 1.1.0",
+                f"  - latest:    1.1.0 - {green('Up to date!')}",
                 "",
                 "Plugins:",
-                f"  - foobar: 1.0.0 - {yellow('Update available!')}",
-                "  Your version of dbt-foobar is out of date! You can find instructions for upgrading here:",
-                "  https://docs.getdbt.com/dbt-cli/install/overview",
+                f"  - foobar: 1.0.0 - {red('Not compatible!')}",
+                "",
+                "  At least one plugin is out of date or incompatible with dbt-core.",
+                "  You can find instructions for upgrading here:",
+                "  https://docs.getdbt.com/docs/installation",
                 "",
                 "",
             ]
@@ -471,15 +502,16 @@ def test_plugin_diff_core_minor_behind_latest(self, mocker):
         actual = dbt.version.get_version_information()
         expected = "\n".join(
             [
-                "installed version: 1.1.0",
-                "   latest version: 1.1.0",
-                "",
-                green("Up to date!"),
+                "Core:",
+                "  - installed: 1.1.0",
+                f"  - latest:    1.1.0 - {green('Up to date!')}",
                 "",
                 "Plugins:",
-                f"  - foobar: 1.0.0 - {yellow('Update available!')}",
-                "  Your version of dbt-foobar is out of date! You can find instructions for upgrading here:",
-                "  https://docs.getdbt.com/dbt-cli/install/overview",
+                f"  - foobar: 1.0.0 - {red('Not compatible!')}",
+                "",
+                "  At least one plugin is out of date or incompatible with dbt-core.",
+                "  You can find instructions for upgrading here:",
+                "  https://docs.getdbt.com/docs/installation",
                 "",
                 "",
             ]
@@ -494,35 +526,63 @@ def test_plugins_various(self, mocker):
             latest="2.1.0",
             plugins={
                 "foobar": ("2.1.0", "2.1.0"),
-                "bazqux": ("1.0.0", None),
-                "quuux": ("1.1.0", "1.1.0"),
-                "corge": ("2.2.2", "2.2.3"),
-                "grault": ("1.1.0", "1.2.3"),
-                "garply": ("2.1.0", None),
+                "bazqux": ("2.1.0", None),
+                "quuux": ("2.1.0", "2.1.0"),
+                "corge": ("22.21.20", "22.21.21"),
+                "grault": ("2.1.0", "2.1.1"),
+                "garply": ("2.1.0-b1", None),
             },
         )
 
         actual = dbt.version.get_version_information()
         expected = "\n".join(
             [
-                "installed version: 2.1.0",
-                "   latest version: 2.1.0",
-                "",
-                green("Up to date!"),
+                "Core:",
+                "  - installed: 2.1.0",
+                f"  - latest:    2.1.0 - {green('Up to date!')}",
                 "",
                 "Plugins:",
-                f"  - foobar: 2.1.0 - {green('Up to date!')}",
-                f"  - bazqux: 1.0.0 - {yellow('No PYPI version available')}",
-                f"  - quuux: 1.1.0 - {green('Up to date!')}",
-                f"  - corge: 2.2.2 - {yellow('Update available!')}",
-                "  Your version of dbt-corge is out of date! You can find instructions for upgrading here:",
-                "  https://docs.getdbt.com/dbt-cli/install/overview",
+                f"  - foobar: 2.1.0    - {green('Up to date!')}",
+                f"  - bazqux: 2.1.0    - {yellow('Could not determine latest version')}",
+                f"  - quuux:  2.1.0    - {green('Up to date!')}",
+                f"  - corge:  22.21.20 - {red('Not compatible!')}",
+                f"  - grault: 2.1.0    - {yellow('Update available!')}",
+                f"  - garply: 2.1.0-b1 - {yellow('Could not determine latest version')}",
+                "",
+                "  At least one plugin is out of date or incompatible with dbt-core.",
+                "  You can find instructions for upgrading here:",
+                "  https://docs.getdbt.com/docs/installation",
+                "",
+                "",
+            ]
+        )
+
+        assert expected == actual
+
+    def test_plugins_alignment(self, mocker):
+        mock_versions(
+            mocker,
+            installed="1.1.1-b123",
+            latest="1.1.1-b123",
+            plugins={
+                "foobar": ("1.1.0-b1", "1.1.0-b1"),
+                "bazqux": ("1.1.1-b123", "1.1.1-b123"),
+                "quuux": ("1.1.0", "1.1.0"),
+            },
+        )
+
+        actual = dbt.version.get_version_information()
+        expected = "\n".join(
+            [
+                "Core:",
+                "  - installed: 1.1.1-b123",
+                f"  - latest:    1.1.1-b123 - {green('Up to date!')}",
                 "",
-                f"  - grault: 1.1.0 - {red('Out of date!')}",
-                "  Your version of dbt-grault is out of date! You can find instructions for upgrading here:",
-                "  https://docs.getdbt.com/dbt-cli/install/overview",
+                "Plugins:",
+                f"  - foobar: 1.1.0-b1   - {green('Up to date!')}",
+                f"  - bazqux: 1.1.1-b123 - {green('Up to date!')}",
+                f"  - quuux:  1.1.0      - {green('Up to date!')}",
                 "",
-                f"  - garply: 2.1.0 - {green('Up to date!')}",
                 "",
             ]
         )
@@ -592,18 +652,18 @@ def mock_import(*args, **kwargs):
         actual = dbt.version.get_version_information()
         expected = "\n".join(
             [
-                "installed version: 1.0.0",
-                "   latest version: 1.0.0",
-                "",
-                green("Up to date!"),
+                "Core:",
+                "  - installed: 1.0.0",
+                f"  - latest:    1.0.0 - {green('Up to date!')}",
                 "",
                 "Plugins:",
-                "  - foobar: 1.0.0 - Up to date!",
-                "  - bazqux: 1.0.0 - Up to date!",
-                "  - quuux: 1.0.0 - Up to date!",
-                "  - corge: 1.0.0 - Up to date!",
-                "  - grault: 1.0.0 - Up to date!",
-                "  - garply: 1.0.0 - Up to date!",
+                f"  - foobar: 1.0.0 - {yellow('Could not determine latest version')}",
+                f"  - bazqux: 1.0.0 - {yellow('Could not determine latest version')}",
+                f"  - quuux:  1.0.0 - {yellow('Could not determine latest version')}",
+                f"  - corge:  1.0.0 - {yellow('Could not determine latest version')}",
+                f"  - grault: 1.0.0 - {yellow('Could not determine latest version')}",
+                f"  - garply: 1.0.0 - {yellow('Could not determine latest version')}",
+                "",
                 "",
             ]
         )