Repository: Sceptre/sceptre

Issue:
Reimplement #1443 - parameter Schema Validation
### Subject of the issue
In already-resolved and merged issue #1443  we implemented parameter validation. In Slack, we then discussed a preferred implementation of using instead a parameter Schema Validator.

This issue tracks possible reimplementation of this feature.

### Your environment
* version of sceptre (sceptre --version) latest
* version of python (python --version) 3.10
* which OS/distro Ubuntu

### More info
```
% git show f3b7e51fee1ff78a457f20b5dddac91254a5fdf2 
```
- Merged PR https://github.com/Sceptre/sceptre/pull/1442
- Slack discussion is in the archives.
- Schema validator suggested was pydantic.

Discussion:

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 5daa10b..d6f8dbb 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -9,7 +9,7 @@ repos:
       - id: mixed-line-ending
       - id: trailing-whitespace
   - repo: https://github.com/PyCQA/flake8
-    rev: 7.0.0
+    rev: 7.1.0
     hooks:
       - id: flake8
   - repo: https://github.com/adrienverge/yamllint
@@ -17,7 +17,7 @@ repos:
     hooks:
       - id: yamllint
   - repo: https://github.com/awslabs/cfn-python-lint
-    rev: v1.2.5a9
+    rev: v1.4.2
     hooks:
       - id: cfn-python-lint
         args:
@@ -47,7 +47,7 @@ repos:
         language_version: python3.10
         args: ['--check']
   - repo: https://github.com/sirosen/check-jsonschema
-    rev: 0.28.4
+    rev: 0.28.6
     hooks:
       - id: check-github-workflows
       - id: check-github-actions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 08fb35b..543141e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -207,14 +207,23 @@ during development.
   to work with an AWS account that you have access to.  You can use the same user
   or role that you use for Github actions.
 
+#### run all tests
+
+```bash
+$ AWS_DEFAULT_REGION=eu-west-1 poetry run behave integration-tests/features --junit --junit-directory build/behave
+```
+
 _Note_: All integration tests are set up to run in `eu-west-*` region.  If you prefer
-to run in a different region you must update the region in each test before running it
+to run in a different region, you must update the region in each test before running it:
 
+```bash
+$ grep -r eu-west integration-tests
+```
 
-#### run all tests
+Edit the files.
 
 ```bash
-$ AWS_PROFILE=<profile> AWS_DEFAULT_REGION=<region> poetry run behave integration-tests/features --junit --junit-directory build/behave
+$ AWS_PROFILE=<your-profile> AWS_DEFAULT_REGION=<your-preferred-default-region> poetry run behave integration-tests/features --junit --junit-directory build/behave
 ```
 
 #### run a specific feature
diff --git a/sceptre/config/reader.py b/sceptre/config/reader.py
index fc74217..0245bc9 100644
--- a/sceptre/config/reader.py
+++ b/sceptre/config/reader.py
@@ -489,6 +489,14 @@ class ConfigReader(object):
                 f"{Path(directory_path, basename).as_posix()} - {err}"
             ) from err
 
+        # Reset the template cache to avoid leakage between StackGroups (#937)
+        template_vars = {"var": self.templating_vars["var"]}
+        if "stack_group_config" in self.templating_vars:
+            template_vars["stack_group_config"] = self.templating_vars[
+                "stack_group_config"
+            ]
+        self.templating_vars = template_vars
+
         self.templating_vars.update(stack_group_config)
 
         try:
diff --git a/sceptre/stack.py b/sceptre/stack.py
index fa5639a..f085ced 100644
--- a/sceptre/stack.py
+++ b/sceptre/stack.py
@@ -263,7 +263,7 @@ class Stack:
         )
 
         self.s3_details = s3_details
-        self.parameters = self._ensure_parameters(parameters or {})
+        self.parameters = self._cast_parameters(parameters or {})
         self.sceptre_user_data = sceptre_user_data or {}
         self.notifications = notifications or []
 
@@ -276,10 +276,21 @@ class Stack:
             )
         return value
 
-    def _ensure_parameters(
+    def _cast_parameters(
         self, parameters: Dict[str, Any]
     ) -> Dict[str, Union[str, List[Union[str, Resolver]], Resolver]]:
-        """Ensure CloudFormation parameters are of valid types"""
+        """Cast CloudFormation parameters to valid types"""
+
+        def cast_value(value: Any) -> Union[str, List[Union[str, Resolver]], Resolver]:
+            if isinstance(value, bool):
+                return "true" if value else "false"
+            elif isinstance(value, (int, float)):
+                return str(value)
+            elif isinstance(value, list):
+                return [cast_value(item) for item in value]
+            elif isinstance(value, Resolver):
+                return value
+            return value
 
         def is_valid(value: Any) -> bool:
             return (
@@ -294,11 +305,14 @@ class Stack:
                 or isinstance(value, Resolver)
             )
 
-        if not all(is_valid(value) for value in parameters.values()):
+        casted_parameters = {k: cast_value(v) for k, v in parameters.items()}
+
+        if not all(is_valid(value) for value in casted_parameters.values()):
             raise InvalidConfigFileError(
-                f"{self.name}: Values for parameters must be strings, lists or resolvers, got {parameters}"
+                f"{self.name}: Values for parameters must be strings, lists or resolvers, got {casted_parameters}"
             )
-        return parameters
+
+        return casted_parameters
 
     def __repr__(self):
         return (


Test patch:
diff --git a/.github/workflows/comment-integration-tests.yaml b/.github/workflows/comment-integration-tests.yaml
deleted file mode 100644
index a05829a..0000000
--- a/.github/workflows/comment-integration-tests.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-# The idea with this workflow is to allow users to trigger an integration test
-# run from a PR however it doesn't work because github action does not allow
-# access to the github token when triggered from a PR.  The workflow fails with..
-# "Credentials could not be loaded, please check your action inputs: Could not load credentials from any providers"
-
-name: comment-integration-tests
-
-on:
-  pull_request_review:
-    types: [submitted]
-
-jobs:
-  integration-tests:
-    if: ${{ contains(github.event.review.body, '/integration-tests') }}
-    uses: "./.github/workflows/integration-tests.yaml"
-    with:
-      # role generated from https://github.com/Sceptre/sceptre-aws/blob/master/config/prod/gh-oidc-sceptre-tests.yaml
-      role-to-assume: "arn:aws:iam::743644221192:role/gh-oidc-sceptre-tests"
diff --git a/tests/test_config_reader.py b/tests/test_config_reader.py
index 1bd9f7e..ddd403f 100644
--- a/tests/test_config_reader.py
+++ b/tests/test_config_reader.py
@@ -621,6 +621,65 @@ class TestConfigReader(object):
 
             assert result == {"key": "value"}
 
+    def test_render__existing_config_file__no_leak_between_multiple_stack_group_configs(
+        self,
+    ):
+        """A test for bug #937"""
+        with self.runner.isolated_filesystem():
+            project_path = os.path.abspath("./example")
+
+            self.context.project_path = project_path
+
+            config_dir = os.path.join(project_path, "config")
+
+            stack_group_config_1 = {"j2_environment": {}, "param1": "value1"}
+            stack_group_config_2 = {"j2_environment": {}, "param2": "value2"}
+
+            directory_path_1 = os.path.join(config_dir, "dir1")
+            directory_path_2 = os.path.join(config_dir, "dir2")
+            os.makedirs(directory_path_1)
+            os.makedirs(directory_path_2)
+
+            config_reader = ConfigReader(self.context)
+
+            # First config file
+            basename_1 = "file1.yaml"
+            test_config_path_1 = os.path.join(directory_path_1, basename_1)
+            test_config_content_1 = "var: initial_value\nparam1: value1"
+            with open(test_config_path_1, "w") as file:
+                file.write(test_config_content_1)
+
+            # Second config file
+            basename_2 = "file2.yaml"
+            test_config_path_2 = os.path.join(directory_path_2, basename_2)
+            test_config_content_2 = "var: initial_value\nparam2: value2"
+            with open(test_config_path_2, "w") as file:
+                file.write(test_config_content_2)
+
+            config_reader.full_config_path = project_path
+            config_reader.templating_vars = {"var": "initial_value"}
+
+            # Run _render for the first stack group
+            result_1 = config_reader._render(
+                "config/dir1", basename_1, stack_group_config_1
+            )
+            expected_result_1 = {"var": "initial_value", "param1": "value1"}
+            assert expected_result_1 == result_1
+
+            # Run _render for the second stack group
+            result_2 = config_reader._render(
+                "config/dir2", basename_2, stack_group_config_2
+            )
+            expected_result_2 = {"var": "initial_value", "param2": "value2"}
+            assert expected_result_2 == result_2
+
+            # Ensure the templating_vars is not leaking
+            assert {
+                "var": "initial_value",
+                "j2_environment": {},
+                "param2": "value2",
+            } == config_reader.templating_vars
+
     def test_render__invalid_jinja_template__raises_and_creates_debug_file(self):
         with self.runner.isolated_filesystem():
             project_path = os.path.abspath("./example")
diff --git a/tests/test_stack.py b/tests/test_stack.py
index 6f67e08..eeddf74 100644
--- a/tests/test_stack.py
+++ b/tests/test_stack.py
@@ -190,12 +190,7 @@ class TestStack(object):
 
     @pytest.mark.parametrize(
         "parameters",
-        [
-            {"someNum": 1},
-            {"someBool": True},
-            {"aBadList": [1, 2, 3]},
-            {"aDict": {"foo": "bar"}},
-        ],
+        [{"Dict": {"foo": "bar"}}, {"List": ["of", "stuff", {"including": "aDict"}]}],
     )
     def test_init__invalid_parameters_raise_invalid_config_file_error(self, parameters):
         with pytest.raises(InvalidConfigFileError):
@@ -210,10 +205,13 @@ class TestStack(object):
     @pytest.mark.parametrize(
         "parameters",
         [
-            {"someNum": "1"},
-            {"someBool": "true"},
-            {"aList": ["aString", FakeResolver()]},
-            {"aResolver": FakeResolver()},
+            {"IntAsString": "1"},
+            {"Int": 1},
+            {"BoolAsString": "true"},
+            {"Bool": True},
+            {"List": ["aStringAndA", FakeResolver()]},
+            {"ListOfInts": [1, 2, 3]},
+            {"Resolver": FakeResolver()},
         ],
     )
     def test_init__valid_parameters_do_not_raise_invalid_config_file_error(