Repository: dbt-labs/dbt-core

Issue:
debug exit code does not match connection test
### Describe the bug
When I run `dbt debug`, I would expect a successful exit code (0) when the connection test succeeds and an error code (>0) when the connection test fails. In `0.18.1` the exit code is always 0, regardless of the result of the test.

### Steps To Reproduce
1. Make sure your profile works
2. Run `dbt debug` (connection test should succeed)
3. Run `echo $?` - observe that the output is `0`
4. Mess up your profile (e.g. change the port to an invalid one)
5. Run `dbt debug` (connection test should fail)
6. Run `echo $?` - observe that the output is still `0`

### Expected behavior
A strictly positive exit code when the connection test fails.

### Screenshots and log output
Mentioned in repro

### System information
Any database

**The output of `dbt --version`:**
```
installed version: 0.18.1
   latest version: 0.18.1

Up to date!

Plugins:
  - sqlserver: 0.18.1
  - bigquery: 0.18.1
  - snowflake: 0.18.1
  - postgres: 0.18.1
  - redshift: 0.18.1
```

**The operating system you're using: Debian Buster

**The output of `python --version`:**
Python 3.8.7


Discussion:
@sdebruyn I agree. If `dbt debug` manages to detect an error in your path, project, connection profile—or that you're not running from a dbt project at all—it feels like it should return `1` ("handled error") as the exit code instead of `0`. I imagine could be useful for plugin maintainers who want to check various connection methods in CI (e.g. https://github.com/dbt-msft/dbt-sqlserver/issues/65).

Is this a change you'd be interested in contributing?
Sure :) Am I correct to assume that these 2 files are the only ones I'd need to change?
- https://github.com/fishtown-analytics/dbt/blob/dev/margaret-mead/test/integration/049_dbt_debug_test/test_debug.py
- https://github.com/fishtown-analytics/dbt/blob/dev/margaret-mead/core/dbt/task/debug.py
That sounds right to me! We define an `ExitCodes` enum here:
https://github.com/fishtown-analytics/dbt/blob/1f927a374c8bd52a12a20d892fed9d59cffd04f4/core/dbt/utils.py#L32-L35

(`ModelError` sounds narrow; we might consider renaming someday to something more generic like `HandledError`.)

I think you'd want to pull that into `task/debug.py`, and conclude the `run` method by calling `sys.exit()` with the appropriate enum value.

Resolving patch:
diff --git a/core/dbt/task/debug.py b/core/dbt/task/debug.py
--- a/core/dbt/task/debug.py
+++ b/core/dbt/task/debug.py
@@ -48,7 +48,6 @@
 {url}
 '''.lstrip()
 
-
 MISSING_PROFILE_MESSAGE = '''
 dbt looked for a profiles.yml file in {path}, but did
 not find one. For more information on configuring your profile, consult the
@@ -90,6 +89,7 @@ def __init__(self, args, config):
         self.profile_name: Optional[str] = None
         self.project: Optional[Project] = None
         self.project_fail_details = ''
+        self.any_failure = False
         self.messages: List[str] = []
 
     @property
@@ -111,7 +111,7 @@ def path_info(self):
     def run(self):
         if self.args.config_dir:
             self.path_info()
-            return
+            return not self.any_failure
 
         version = get_installed_version().to_version_string(skip_matcher=True)
         print('dbt version: {}'.format(version))
@@ -129,6 +129,11 @@ def run(self):
             print(message)
             print('')
 
+        return not self.any_failure
+
+    def interpret_results(self, results):
+        return results
+
     def _load_project(self):
         if not os.path.exists(self.project_path):
             self.project_fail_details = FILE_NOT_FOUND
@@ -245,6 +250,7 @@ def _load_profile(self):
             self.messages.append(MISSING_PROFILE_MESSAGE.format(
                 path=self.profile_path, url=ProfileConfigDocs
             ))
+            self.any_failure = True
             return red('ERROR not found')
 
         try:
@@ -283,6 +289,7 @@ def test_git(self):
             dbt.clients.system.run_cmd(os.getcwd(), ['git', '--help'])
         except dbt.exceptions.ExecutableError as exc:
             self.messages.append('Error from git --help: {!s}'.format(exc))
+            self.any_failure = True
             return red('ERROR')
         return green('OK found')
 
@@ -310,6 +317,8 @@ def test_configuration(self):
     def _log_project_fail(self):
         if not self.project_fail_details:
             return
+
+        self.any_failure = True
         if self.project_fail_details == FILE_NOT_FOUND:
             return
         print('Project loading failed for the following reason:')
@@ -319,6 +328,8 @@ def _log_project_fail(self):
     def _log_profile_fail(self):
         if not self.profile_fail_details:
             return
+
+        self.any_failure = True
         if self.profile_fail_details == FILE_NOT_FOUND:
             return
         print('Profile loading failed for the following reason:')
@@ -347,6 +358,7 @@ def _connection_result(self):
         result = self.attempt_connection(self.profile)
         if result is not None:
             self.messages.append(result)
+            self.any_failure = True
             return red('ERROR')
         return green('OK connection ok')
 


Test patch:
diff --git a/test/integration/049_dbt_debug_test/test_debug.py b/test/integration/049_dbt_debug_test/test_debug.py
--- a/test/integration/049_dbt_debug_test/test_debug.py
+++ b/test/integration/049_dbt_debug_test/test_debug.py
@@ -66,17 +66,17 @@ def test_postgres_ok(self):
 
     @use_profile('postgres')
     def test_postgres_nopass(self):
-        self.run_dbt(['debug', '--target', 'nopass'])
+        self.run_dbt(['debug', '--target', 'nopass'], expect_pass=False)
         self.assertGotValue(re.compile(r'\s+profiles\.yml file'), 'ERROR invalid')
 
     @use_profile('postgres')
     def test_postgres_wronguser(self):
-        self.run_dbt(['debug', '--target', 'wronguser'])
+        self.run_dbt(['debug', '--target', 'wronguser'], expect_pass=False)
         self.assertGotValue(re.compile(r'\s+Connection test'), 'ERROR')
 
     @use_profile('postgres')
     def test_postgres_empty_target(self):
-        self.run_dbt(['debug', '--target', 'none_target'])
+        self.run_dbt(['debug', '--target', 'none_target'], expect_pass=False)
         self.assertGotValue(re.compile(r"\s+output 'none_target'"), 'misconfigured')
 
 
@@ -110,7 +110,7 @@ def capsys(self, capsys):
     def test_postgres_empty_project(self):
         with open('dbt_project.yml', 'w') as f:
             pass
-        self.run_dbt(['debug', '--profile', 'test'])
+        self.run_dbt(['debug', '--profile', 'test'], expect_pass=False)
         splitout = self.capsys.readouterr().out.split('\n')
         for line in splitout:
             if line.strip().startswith('dbt_project.yml file'):
@@ -124,7 +124,7 @@ def test_postgres_badproject(self):
         self.use_default_project(overrides={
             'invalid-key': 'not a valid key so this is bad project',
         })
-        self.run_dbt(['debug', '--profile', 'test'])
+        self.run_dbt(['debug', '--profile', 'test'], expect_pass=False)
         splitout = self.capsys.readouterr().out.split('\n')
         for line in splitout:
             if line.strip().startswith('dbt_project.yml file'):
@@ -134,7 +134,7 @@ def test_postgres_badproject(self):
 
     @use_profile('postgres')
     def test_postgres_not_found_project_dir(self):
-        self.run_dbt(['debug', '--project-dir', 'nopass'])
+        self.run_dbt(['debug', '--project-dir', 'nopass'], expect_pass=False)
         splitout = self.capsys.readouterr().out.split('\n')
         for line in splitout:
             if line.strip().startswith('dbt_project.yml file'):
@@ -151,7 +151,7 @@ def test_postgres_invalid_project_outside_current_dir(self):
         os.makedirs('custom', exist_ok=True)
         with open("custom/dbt_project.yml", 'w') as f:
             yaml.safe_dump(project_config, f, default_flow_style=True)
-        self.run_dbt(['debug', '--project-dir', 'custom'])
+        self.run_dbt(['debug', '--project-dir', 'custom'], expect_pass=False)
         splitout = self.capsys.readouterr().out.split('\n')
         for line in splitout:
             if line.strip().startswith('dbt_project.yml file'):