Repository: SFDO-Tooling/CumulusCI
Pull request #3653 (2023-09-19 11:12:00)

Feature/deploy release specification for scratch org creation


Patch:
diff --git a/cumulusci/cli/org.py b/cumulusci/cli/org.py
index c8ee718e6..301e42358 100644
--- a/cumulusci/cli/org.py
+++ b/cumulusci/cli/org.py
@@ -563,9 +563,20 @@ def org_remove(runtime, org_name, global_org):
 @click.option(
     "--no-password", is_flag=True, help="If set, don't set a password for the org"
 )
+@click.option(
+    "--release",
+    help="If provided, specify either previous or preview when creating a scratch org",
+)
 @pass_runtime(require_keychain=True)
-def org_scratch(runtime, config_name, org_name, default, devhub, days, no_password):
+def org_scratch(
+    runtime, config_name, org_name, default, devhub, days, no_password, release
+):
     runtime.check_org_overwrite(org_name)
+    release_options = ["previous", "preview"]
+    if release and release not in release_options:
+        raise click.UsageError(
+            "Release options value is not valid. Either specify preview or previous."
+        )
 
     scratch_configs = runtime.project_config.lookup("orgs__scratch")
     if not scratch_configs:
@@ -580,7 +591,7 @@ def org_scratch(runtime, config_name, org_name, default, devhub, days, no_passwo
         scratch_config["devhub"] = devhub
 
     runtime.keychain.create_scratch_org(
-        org_name, config_name, days, set_password=not (no_password)
+        org_name, config_name, days, set_password=not (no_password), release=release
     )
 
     if default:
diff --git a/cumulusci/core/config/scratch_org_config.py b/cumulusci/core/config/scratch_org_config.py
index 06e81198d..edd0c1880 100644
--- a/cumulusci/core/config/scratch_org_config.py
+++ b/cumulusci/core/config/scratch_org_config.py
@@ -23,6 +23,7 @@ class ScratchOrgConfig(SfdxOrgConfig):
     instance: str
     password_failed: bool
     devhub: str
+    release: str
 
     createable: bool = True
 
@@ -145,6 +146,8 @@ class ScratchOrgConfig(SfdxOrgConfig):
             args += ["--noancestors"]
         if self.days:
             args += ["--durationdays", str(self.days)]
+        if self.release:
+            args += [f"release={self.release}"]
         if self.sfdx_alias:
             args += ["-a", self.sfdx_alias]
         with open(self.config_file, "r") as org_def:
diff --git a/cumulusci/core/keychain/base_project_keychain.py b/cumulusci/core/keychain/base_project_keychain.py
index 78f40a903..561caeaec 100644
--- a/cumulusci/core/keychain/base_project_keychain.py
+++ b/cumulusci/core/keychain/base_project_keychain.py
@@ -54,7 +54,9 @@ class BaseProjectKeychain(BaseConfig):
     #               Orgs                  #
     #######################################
 
-    def create_scratch_org(self, org_name, config_name, days=None, set_password=True):
+    def create_scratch_org(
+        self, org_name, config_name, days=None, set_password=True, release=None
+    ):
         """Adds/Updates a scratch org config to the keychain from a named config"""
         scratch_config = self.project_config.lookup(f"orgs__scratch__{config_name}")
         if scratch_config is None:
@@ -65,16 +67,20 @@ class BaseProjectKeychain(BaseConfig):
         else:
             # Use scratch config days or default of 1 day
             scratch_config.setdefault("days", 1)
+        if release is not None:
+            scratch_config["release"] = release
         scratch_config["set_password"] = bool(set_password)
         scratch_config["scratch"] = True
         scratch_config.setdefault("namespaced", False)
         scratch_config["config_name"] = config_name
+
         scratch_config[
             "sfdx_alias"
         ] = f"{self.project_config.project__name}__{org_name}"
         org_config = ScratchOrgConfig(
             scratch_config, org_name, keychain=self, global_org=False
         )
+
         org_config.save()
 
     def set_org(self, org_config, global_org=False, save=True):
diff --git a/cumulusci/schema/cumulusci.jsonschema.json b/cumulusci/schema/cumulusci.jsonschema.json
index 7d53f2c25..255d2de4d 100644
--- a/cumulusci/schema/cumulusci.jsonschema.json
+++ b/cumulusci/schema/cumulusci.jsonschema.json
@@ -430,6 +430,11 @@
                 "noancestors": {
                     "title": "Noancestors",
                     "type": "boolean"
+                },
+                "release": {
+                    "title": "Release",
+                    "enum": ["preview", "previous"],
+                    "type": "string"
                 }
             },
             "additionalProperties": false
diff --git a/cumulusci/utils/yaml/cumulusci_yml.py b/cumulusci/utils/yaml/cumulusci_yml.py
index a85cee77b..f8498ed9e 100644
--- a/cumulusci/utils/yaml/cumulusci_yml.py
+++ b/cumulusci/utils/yaml/cumulusci_yml.py
@@ -150,6 +150,7 @@ class ScratchOrg(CCIDictModel):
     namespaced: str = None
     setup_flow: str = None
     noancestors: bool = None
+    release: Literal["preview", "previous"] = None
 
 
 class Orgs(CCIDictModel):


Test patch:
diff --git a/cumulusci/cli/tests/test_org.py b/cumulusci/cli/tests/test_org.py
index 007e82249..c6ed45ed8 100644
--- a/cumulusci/cli/tests/test_org.py
+++ b/cumulusci/cli/tests/test_org.py
@@ -1249,20 +1249,40 @@ class TestOrgCommands:
             devhub="hub",
             days=7,
             no_password=True,
+            release="previous",
         )
 
         runtime.check_org_overwrite.assert_called_once()
         runtime.keychain.create_scratch_org.assert_called_with(
-            "test", "dev", 7, set_password=False
+            "test", "dev", 7, set_password=False, release="previous"
         )
         runtime.keychain.set_default_org.assert_called_with("test")
 
-    def test_org_scratch__not_default(self):
+    def test_org_scratch_release_invalid(self):
         runtime = mock.Mock()
+
         runtime.project_config.lookup = MockLookup(
             orgs__scratch={"dev": {"orgName": "Dev"}}
         )
+        with pytest.raises(click.UsageError):
+            run_click_command(
+                org.org_scratch,
+                runtime=runtime,
+                config_name="dev",
+                org_name="test",
+                default=True,
+                devhub="hub",
+                days=7,
+                no_password=True,
+                release="next",
+            )
+        runtime.check_org_overwrite.assert_called_once()
 
+    def test_org_scratch__not_default(self):
+        runtime = mock.Mock()
+        runtime.project_config.lookup = MockLookup(
+            orgs__scratch={"dev": {"orgName": "Dev"}}
+        )
         run_click_command(
             org.org_scratch,
             runtime=runtime,
@@ -1272,11 +1292,12 @@ class TestOrgCommands:
             devhub="hub",
             days=7,
             no_password=True,
+            release=None,
         )
 
         runtime.check_org_overwrite.assert_called_once()
         runtime.keychain.create_scratch_org.assert_called_with(
-            "test", "dev", 7, set_password=False
+            "test", "dev", 7, set_password=False, release=None
         )
 
     def test_org_scratch_no_configs(self):
@@ -1293,6 +1314,7 @@ class TestOrgCommands:
                 devhub="hub",
                 days=7,
                 no_password=True,
+                release="previous",
             )
 
     def test_org_scratch_config_not_found(self):
@@ -1309,6 +1331,7 @@ class TestOrgCommands:
                 devhub="hub",
                 days=7,
                 no_password=True,
+                release="previous",
             )
 
     def test_org_scratch_delete(self):
diff --git a/cumulusci/core/config/tests/test_config_expensive.py b/cumulusci/core/config/tests/test_config_expensive.py
index 13485a7b7..7c3e879fc 100644
--- a/cumulusci/core/config/tests/test_config_expensive.py
+++ b/cumulusci/core/config/tests/test_config_expensive.py
@@ -793,6 +793,7 @@ class TestScratchOrgConfigPytest:
                 "sfdx_alias": "project__org",
                 "default": True,
                 "instance": "NA01",
+                "release": "previous",
             },
             "test",
             mock_keychain,
@@ -809,6 +810,7 @@ class TestScratchOrgConfigPytest:
             "--noancestors",
             "--durationdays",
             "1",
+            "release=previous",
             "-a",
             "project__org",
             "adminEmail=test@example.com",
diff --git a/cumulusci/core/keychain/tests/test_base_project_keychain.py b/cumulusci/core/keychain/tests/test_base_project_keychain.py
index 06e9757fd..89cc12318 100644
--- a/cumulusci/core/keychain/tests/test_base_project_keychain.py
+++ b/cumulusci/core/keychain/tests/test_base_project_keychain.py
@@ -152,9 +152,10 @@ class TestBaseProjectKeychain:
         )
         keychain = BaseProjectKeychain(project_config, key)
         keychain.key = key
-        keychain.create_scratch_org("test", "dev", days=3)
+        keychain.create_scratch_org("test", "dev", days=3, release="previous")
         org_config = keychain.get_org("test").config
         assert org_config["days"] == 3
+        assert org_config["release"] == "previous"
 
     def test_load_scratch_orgs(self, keychain):
         assert list(keychain.orgs) == []