Repository: SFDO-Tooling/CumulusCI (python)

Issue:
Creating Scratch Org fails - No authorization information found for username
### Describe the bug

Running the command `cci org info dev_namespaced` results in an error:

```
Failed to set password:                                                                                                                               
                                                                                                                                                                          
                    ERROR running force:user:password:generate:  No authorization information found for test-yfscjgi5p3kb@example.com.
```

### Reproduction steps

1. run `cci org info dev_namespaced`
2. observe cli error


### Your CumulusCI and Python versions

CumulusCI version: 3.63.0 (/Users/rf/.local/bin/cci)
Python version: 3.10.6 (/Users/rf/.local/pipx/venvs/cumulusci/bin/python)

### Operating System

MacOS

### Windows environment

_No response_

### CumulusCI installation method

pipx

### Error Gist

https://gist.github.com/ryanfollmer/b58380ff1a21bf66d7c2a433357f8ee1

### Additional information

sfdx version: sfdx-cli/7.164.2 darwin-x64 node-v16.17.0

Pull request:
Fix Scratch org creation in latest sfdx
Old code parsed human-readable output. New code parses JSON. 

Fixes #3332

Patch:
diff --git a/cumulusci/core/config/scratch_org_config.py b/cumulusci/core/config/scratch_org_config.py
index 1a6b57aff..480434c24 100644
--- a/cumulusci/core/config/scratch_org_config.py
+++ b/cumulusci/core/config/scratch_org_config.py
@@ -1,7 +1,6 @@
 import datetime
 import json
 import os
-import re
 
 from cumulusci.core.config import FAILED_TO_CREATE_SCRATCH_ORG
 from cumulusci.core.config.sfdx_org_config import SfdxOrgConfig
@@ -71,38 +70,49 @@ class ScratchOrgConfig(SfdxOrgConfig):
         args = self._build_org_create_args()
         extra_args = os.environ.get("SFDX_ORG_CREATE_ARGS", "")
         p = sfdx(
-            f"force:org:create {extra_args}",
+            f"force:org:create --json {extra_args}",
             args=args,
             username=None,
             log_note="Creating scratch org",
         )
+        stdout = p.stdout_text.read()
+        stderr = p.stderr_text.read()
 
-        stderr = [line.strip() for line in p.stderr_text]
-        stdout = [line.strip() for line in p.stdout_text]
-
-        if p.returncode:
-            message = f"{FAILED_TO_CREATE_SCRATCH_ORG}: \n{nl.join(stdout)}\n{nl.join(stderr)}"
+        def raise_error():
+            message = f"{FAILED_TO_CREATE_SCRATCH_ORG}: \n{stdout}\n{stderr}"
             raise ScratchOrgException(message)
 
-        re_obj = re.compile("Successfully created scratch org: (.+), username: (.+)")
-        username = None
-        for line in stdout:
-            match = re_obj.search(line)
-            if match:
-                self.config["org_id"] = match.group(1)
-                self.config["username"] = username = match.group(2)
-            self.logger.info(line)
-        for line in stderr:
-            self.logger.error(line)
-
-        if username is None:
+        if p.returncode:
+            raise_error()
+        try:
+            result = json.loads(stdout)
+        except json.decoder.JSONDecodeError:
+            raise_error()
+
+        if (
+            not (res := result.get("result"))
+            or ("username" not in res)
+            or ("orgId" not in res)
+        ):
+            raise_error()
+
+        if res["username"] is None:
             raise ScratchOrgException(
                 "SFDX claimed to be successful but there was no username "
                 "in the output...maybe there was a gack?"
             )
 
+        self.config["org_id"] = res["orgId"]
+        self.config["username"] = res["username"]
+
         self.config["date_created"] = datetime.datetime.utcnow()
 
+        self.logger.error(stderr)
+
+        self.logger.info(
+            f"Created: OrgId: {self.config['org_id']}, Username:{self.config['username']}"
+        )
+
         if self.config.get("set_password"):
             self.generate_password()
 


Test patch:
diff --git a/cumulusci/core/config/tests/test_config_expensive.py b/cumulusci/core/config/tests/test_config_expensive.py
index cddc1b607..0e558ebcb 100644
--- a/cumulusci/core/config/tests/test_config_expensive.py
+++ b/cumulusci/core/config/tests/test_config_expensive.py
@@ -561,7 +561,14 @@ class TestScratchOrgConfig:
         assert not config.active
 
     def test_create_org(self, Command):
-        out = b"Successfully created scratch org: ORG_ID, username: USERNAME"
+        out = b"""{
+                "status": 0,
+                "result": {
+                    "orgId": "ORG_ID",
+                    "username": "USERNAME"
+                }
+                }
+                """
         Command.return_value = p = mock.Mock(
             stdout=io.BytesIO(out), stderr=io.BytesIO(b""), returncode=0
         )