Repository: canonical/charmcraft

Issue:
Doesn't support nested `any-of` expressions in `metadata.yaml` `assumes`
### Bug Description

Juju supports nested `any-of` and `all-of` expressions in `metadata.yaml`'s `assumes:`

charmcraft fails to pack charms with nested expressions.

If you pack a charm with charmcraft, unzip the file, add the nested expression to metadata.yaml, rezip the file, and deploy it with Juju, Juju correctly reads the nested expressions

### To Reproduce

1. `charmcraft init`
2. Append
```yaml
assumes:
  - any-of:
      - all-of:
          - juju >= 2.9.44
          - juju < 3
      - all-of:
          - juju >= 3.1.6
          - juju < 4
```
to `metadata.yaml`
3. `charmcraft pack`
4. 
```
Bad metadata.yaml content:
- string type expected in field 'assumes[0]'
- string type expected in field 'assumes[0].any-of[0]'
- string type expected in field 'assumes[0].any-of[1]'
```

### Environment

Ubuntu 22.04 LTS

### charmcraft.yaml

```shell
# This file configures Charmcraft.
# See https://juju.is/docs/sdk/charmcraft-config for guidance.

type: charm
bases:
  - build-on:
    - name: ubuntu
      channel: "22.04"
    run-on:
    - name: ubuntu
      channel: "22.04"
```


### Relevant log output

```shell
2023-08-11 14:19:12.973 Starting charmcraft version 2.4.0
2023-08-11 14:19:12.974 Reading '/home/ubuntu/bar2/metadata.yaml'
2023-08-11 14:19:12.977 Validating metadata keys
2023-08-11 14:19:12.978 Bad metadata.yaml content:
- string type expected in field 'assumes[0]'
- string type expected in field 'assumes[0].any-of[0]'
- string type expected in field 'assumes[0].any-of[1]'
2023-08-11 14:19:12.978 Full execution log: '/home/ubuntu/.local/state/charmcraft/log/charmcraft-20230811-141912.972111.log'
```


Discussion:

Patch:
diff --git a/charmcraft/metafiles/actions.py b/charmcraft/metafiles/actions.py
index 95d5bf22..31d2bbe3 100644
--- a/charmcraft/metafiles/actions.py
+++ b/charmcraft/metafiles/actions.py
@@ -16,15 +16,18 @@
 
 """Charmcraft project handle actions.yaml file."""
 
+import contextlib
 import pathlib
 import logging
 import shutil
 from typing import Optional, TYPE_CHECKING
 
+import pydantic
 import yaml
 
 from craft_cli import emit, CraftError
 from charmcraft.const import JUJU_ACTIONS_FILENAME
+from charmcraft.format import format_pydantic_errors
 from charmcraft.models.actions import JujuActions
 from charmcraft.metafiles import read_yaml
 
@@ -34,7 +37,7 @@ if TYPE_CHECKING:
 logger = logging.getLogger(__name__)
 
 
-def parse_actions_yaml(charm_dir: pathlib.Path) -> Optional[JujuActions]:
+def parse_actions_yaml(charm_dir: pathlib.Path, allow_broken=False) -> Optional[JujuActions]:
     """Parse project's actions.yaml.
 
     :param charm_dir: Directory to read actions.yaml from.
@@ -51,7 +54,17 @@ def parse_actions_yaml(charm_dir: pathlib.Path) -> Optional[JujuActions]:
         raise CraftError(f"Cannot read the {JUJU_ACTIONS_FILENAME} file: {exc!r}") from exc
 
     emit.debug(f"Validating {JUJU_ACTIONS_FILENAME}")
-    return JujuActions.parse_obj({"actions": actions, "legacy": True})
+    try:
+        return JujuActions.parse_obj({"actions": actions})
+    except pydantic.ValidationError as error:
+        if allow_broken:
+            emit.progress(
+                format_pydantic_errors(error.errors(), file_name=JUJU_ACTIONS_FILENAME),
+                permanent=True,
+            )
+            emit.debug(f"Ignoring {JUJU_ACTIONS_FILENAME}")
+            return None
+        raise
 
 
 def create_actions_yaml(
@@ -65,23 +78,24 @@ def create_actions_yaml(
 
     :returns: Path to created actions.yaml.
     """
-    if charmcraft_config.actions is None or charmcraft_config.actions.actions is None:
-        return None
-
-    file_path = basedir / JUJU_ACTIONS_FILENAME
-
-    if charmcraft_config.actions.legacy:
-        try:
-            shutil.copyfile(charmcraft_config.project.dirpath / JUJU_ACTIONS_FILENAME, file_path)
-        except shutil.SameFileError:
-            pass
+    original_file_path = charmcraft_config.project.dirpath / JUJU_ACTIONS_FILENAME
+    target_file_path = basedir / JUJU_ACTIONS_FILENAME
+
+    # Copy actions.yaml if it exists, otherwise create it from CharmcraftConfig.
+    if original_file_path.exists():
+        # In the build / test process, the original file may be the same as the target file.
+        with contextlib.suppress(shutil.SameFileError):
+            shutil.copyfile(original_file_path, target_file_path)
     else:
-        file_path.write_text(
-            yaml.dump(
-                charmcraft_config.actions.dict(
-                    include={"actions"}, exclude_none=True, by_alias=True
-                )["actions"]
+        if charmcraft_config.actions:
+            target_file_path.write_text(
+                yaml.dump(
+                    charmcraft_config.actions.dict(
+                        include={"actions"}, exclude_none=True, by_alias=True
+                    )["actions"]
+                )
             )
-        )
+        else:
+            return None
 
-    return file_path
+    return target_file_path
diff --git a/charmcraft/metafiles/config.py b/charmcraft/metafiles/config.py
index f913d9ad..b2bd6bf6 100644
--- a/charmcraft/metafiles/config.py
+++ b/charmcraft/metafiles/config.py
@@ -16,15 +16,18 @@
 
 """Charmcraft project handle config.yaml file."""
 
+import contextlib
 import pathlib
 import logging
 import shutil
 from typing import Optional, TYPE_CHECKING
 
+import pydantic
 import yaml
 
 from craft_cli import emit, CraftError
 from charmcraft.const import JUJU_CONFIG_FILENAME
+from charmcraft.format import format_pydantic_errors
 from charmcraft.models.config import JujuConfig
 from charmcraft.metafiles import read_yaml
 
@@ -34,7 +37,7 @@ if TYPE_CHECKING:
 logger = logging.getLogger(__name__)
 
 
-def parse_config_yaml(charm_dir: pathlib.Path) -> Optional[JujuConfig]:
+def parse_config_yaml(charm_dir: pathlib.Path, allow_broken=False) -> Optional[JujuConfig]:
     """Parse project's config.yaml.
 
     :param charm_dir: Directory to read config.yaml from.
@@ -51,7 +54,17 @@ def parse_config_yaml(charm_dir: pathlib.Path) -> Optional[JujuConfig]:
         raise CraftError(f"Cannot read the {JUJU_CONFIG_FILENAME} file: {exc!r}") from exc
 
     emit.debug(f"Validating {JUJU_CONFIG_FILENAME}")
-    return JujuConfig.parse_obj({"legacy": True, **config})
+    try:
+        return JujuConfig.parse_obj(config)
+    except pydantic.ValidationError as error:
+        if allow_broken:
+            emit.progress(
+                format_pydantic_errors(error.errors(), file_name=JUJU_CONFIG_FILENAME),
+                permanent=True,
+            )
+            emit.debug(f"Ignoring {JUJU_CONFIG_FILENAME}")
+            return None
+        raise
 
 
 def create_config_yaml(
@@ -65,23 +78,24 @@ def create_config_yaml(
 
     :returns: Path to created config.yaml.
     """
-    if charmcraft_config.config is None or charmcraft_config.config.options is None:
-        return None
-
-    file_path = basedir / JUJU_CONFIG_FILENAME
-
-    if charmcraft_config.config.legacy:
-        try:
-            shutil.copyfile(charmcraft_config.project.dirpath / JUJU_CONFIG_FILENAME, file_path)
-        except shutil.SameFileError:
-            pass
+    original_file_path = charmcraft_config.project.dirpath / JUJU_CONFIG_FILENAME
+    target_file_path = basedir / JUJU_CONFIG_FILENAME
+
+    # Copy config.yaml if it exists, otherwise create it from CharmcraftConfig.
+    if original_file_path.exists():
+        # In the build / test process, the original file may be the same as the target file.
+        with contextlib.suppress(shutil.SameFileError):
+            shutil.copyfile(original_file_path, target_file_path)
     else:
-        file_path.write_text(
-            yaml.dump(
-                charmcraft_config.config.dict(
-                    include={"options"}, exclude_none=True, by_alias=True
+        if charmcraft_config.config:
+            target_file_path.write_text(
+                yaml.dump(
+                    charmcraft_config.config.dict(
+                        include={"options"}, exclude_none=True, by_alias=True
+                    )
                 )
             )
-        )
+        else:
+            return None
 
-    return file_path
+    return target_file_path
diff --git a/charmcraft/metafiles/metadata.py b/charmcraft/metafiles/metadata.py
index 6041a539..bf1ca5c2 100644
--- a/charmcraft/metafiles/metadata.py
+++ b/charmcraft/metafiles/metadata.py
@@ -16,15 +16,18 @@
 
 """Handlers for metadata.yaml file."""
 
+import contextlib
 import shutil
 import pathlib
 import logging
 from typing import Any, Dict, TYPE_CHECKING
 
+import pydantic
 import yaml
 from craft_cli import emit, CraftError
 
 from charmcraft.const import METADATA_FILENAME, CHARM_METADATA_KEYS, CHARM_METADATA_KEYS_ALIAS
+from charmcraft.format import format_pydantic_errors
 from charmcraft.models.metadata import CharmMetadataLegacy, BundleMetadataLegacy
 
 if TYPE_CHECKING:
@@ -44,7 +47,9 @@ def read_metadata_yaml(charm_dir: pathlib.Path) -> Dict[str, Any]:
         return yaml.safe_load(fh)
 
 
-def parse_charm_metadata_yaml(charm_dir: pathlib.Path) -> CharmMetadataLegacy:
+def parse_charm_metadata_yaml(
+    charm_dir: pathlib.Path, allow_basic: bool = False
+) -> CharmMetadataLegacy:
     """Parse project's legacy metadata.yaml that used for charms.
 
     :returns: a CharmMetadataLegacy object.
@@ -59,7 +64,19 @@ def parse_charm_metadata_yaml(charm_dir: pathlib.Path) -> CharmMetadataLegacy:
         raise CraftError(f"The {charm_dir / METADATA_FILENAME} file is not valid YAML.")
 
     emit.debug("Validating metadata keys")
-    return CharmMetadataLegacy.unmarshal(metadata)
+    try:
+        return CharmMetadataLegacy.unmarshal(metadata)
+    except pydantic.ValidationError as error:
+        if allow_basic:
+            emit.progress(
+                format_pydantic_errors(error.errors(), file_name=METADATA_FILENAME), permanent=True
+            )
+            emit.debug("Falling back to basic metadata.yaml")
+            metadata_basic = {
+                k: v for k, v in metadata.items() if k in ("name", "summary", "description")
+            }
+            return CharmMetadataLegacy.unmarshal(metadata_basic)
+        raise
 
 
 def parse_bundle_metadata_yaml(charm_dir: pathlib.Path) -> BundleMetadataLegacy:
@@ -93,13 +110,14 @@ def create_metadata_yaml(
 
     :returns: Path to created metadata.yaml.
     """
-    file_path = charm_dir / METADATA_FILENAME
-    # metadata.yaml should be copied if it exists in the project
-    if charmcraft_config.metadata_legacy:
-        try:
-            shutil.copyfile(charmcraft_config.project.dirpath / METADATA_FILENAME, file_path)
-        except shutil.SameFileError:
-            pass
+    original_file_path = charmcraft_config.project.dirpath / METADATA_FILENAME
+    target_file_path = charm_dir / METADATA_FILENAME
+
+    # Copy metadata.yaml if it exists, otherwise create it from CharmcraftConfig.
+    if original_file_path.exists():
+        # In the build / test process, the original file may be the same as the target file.
+        with contextlib.suppress(shutil.SameFileError):
+            shutil.copyfile(original_file_path, target_file_path)
     else:
         # metadata.yaml not exists, create it from config
         metadata = charmcraft_config.dict(
@@ -124,6 +142,6 @@ def create_metadata_yaml(
             if (website := links.pop("website", None)) is not None:
                 metadata["website"] = website
 
-        file_path.write_text(yaml.dump(metadata))
+        target_file_path.write_text(yaml.dump(metadata))
 
-    return file_path
+    return target_file_path
diff --git a/charmcraft/models/actions.py b/charmcraft/models/actions.py
index 90a0b7e4..3223569c 100644
--- a/charmcraft/models/actions.py
+++ b/charmcraft/models/actions.py
@@ -34,7 +34,6 @@ class JujuActions(ModelConfigDefaults):
 
     _action_name_regex = re.compile(r"^[a-zA-Z_][a-zA-Z0-9-_]*$")
     actions: Optional[Dict[str, Dict]]
-    legacy: bool = False
 
     @pydantic.validator("actions")
     def validate_actions(cls, actions, values):
diff --git a/charmcraft/models/charmcraft.py b/charmcraft/models/charmcraft.py
index c7a160ce..7db10715 100644
--- a/charmcraft/models/charmcraft.py
+++ b/charmcraft/models/charmcraft.py
@@ -125,7 +125,7 @@ class CharmcraftConfig(
     bases: Optional[List[BasesConfiguration]]
     analysis: AnalysisConfig = AnalysisConfig()
     actions: Optional[JujuActions]
-    assumes: Optional[List[Union[str, Dict[str, List[str]]]]]
+    assumes: Optional[List[Union[str, Dict[str, Union[List, Dict]]]]]
     containers: Optional[Dict[str, Any]]
     devices: Optional[Dict[str, Any]]
     title: Optional[pydantic.StrictStr]
@@ -223,7 +223,7 @@ class CharmcraftConfig(
         And individual "actions.yaml" should not exists when actions
         is defined in charmcraft.yaml.
         """
-        actions_yaml = parse_actions_yaml(values["project"].dirpath)
+        actions_yaml = parse_actions_yaml(values["project"].dirpath, allow_broken=True)
         if actions is None:
             return actions_yaml
         else:
@@ -233,7 +233,7 @@ class CharmcraftConfig(
                     "defined in 'charmcraft.yaml'"
                 )
 
-            return JujuActions.parse_obj({"actions": actions, "legacy": False})
+            return JujuActions.parse_obj({"actions": actions})
 
     @pydantic.validator("config", pre=True, always=True)
     def validate_config(cls, config, values):
@@ -243,7 +243,7 @@ class CharmcraftConfig(
         And individual "actions.yaml" should not exists when actions
         is defined in charmcraft.yaml.
         """
-        config_yaml = parse_config_yaml(values["project"].dirpath)
+        config_yaml = parse_config_yaml(values["project"].dirpath, allow_broken=True)
         if config is None:
             return config_yaml
         else:
@@ -253,7 +253,7 @@ class CharmcraftConfig(
                     "defined in 'charmcraft.yaml'"
                 )
 
-            return JujuConfig.parse_obj({"legacy": False, **config})
+            return JujuConfig.parse_obj(config)
 
     @classmethod
     def expand_short_form_bases(cls, bases: List[Dict[str, Any]]) -> None:
@@ -267,7 +267,7 @@ class CharmcraftConfig(
 
             try:
                 converted_base = Base(**base)
-            except pydantic.error_wrappers.ValidationError as error:
+            except pydantic.ValidationError as error:
                 # Rewrite location to assist user.
                 pydantic_errors = error.errors()
                 for pydantic_error in pydantic_errors:
@@ -308,7 +308,7 @@ class CharmcraftConfig(
                         )
 
                 if obj.get("type") == "charm":
-                    metadata_legacy = parse_charm_metadata_yaml(project.dirpath)
+                    metadata_legacy = parse_charm_metadata_yaml(project.dirpath, allow_basic=True)
 
                     # need to copy 3 fields from metadata_legacy to charmcraft config
                     return cls.parse_obj(
@@ -340,7 +340,7 @@ class CharmcraftConfig(
                     pass
 
             return cls.parse_obj({"project": project, **obj})
-        except pydantic.error_wrappers.ValidationError as error:
+        except pydantic.ValidationError as error:
             raise CraftError(format_pydantic_errors(error.errors()))
 
     @classmethod
diff --git a/charmcraft/models/config.py b/charmcraft/models/config.py
index fa3d1b37..36b425a5 100644
--- a/charmcraft/models/config.py
+++ b/charmcraft/models/config.py
@@ -30,7 +30,6 @@ class JujuConfig(ModelConfigDefaults):
     """
 
     options: Optional[Dict[str, Dict]]
-    legacy: bool = False
 
     @pydantic.validator("options", pre=True)
     def validate_actions(cls, options, values):
diff --git a/charmcraft/models/metadata.py b/charmcraft/models/metadata.py
index f6c72f2b..b8d2548b 100644
--- a/charmcraft/models/metadata.py
+++ b/charmcraft/models/metadata.py
@@ -21,7 +21,6 @@ from typing import Any, Dict, Optional, List, Union
 import pydantic
 
 from craft_cli import CraftError
-from charmcraft.format import format_pydantic_errors
 from charmcraft.const import METADATA_FILENAME
 
 
@@ -43,7 +42,7 @@ class CharmMetadataLegacy(
     name: pydantic.StrictStr
     summary: pydantic.StrictStr
     description: pydantic.StrictStr
-    assumes: Optional[List[Union[str, Dict[str, List[str]]]]]
+    assumes: Optional[List[Union[str, Dict[str, Union[List, Dict]]]]]
     containers: Optional[Dict[str, Any]]
     devices: Optional[Dict[str, Any]]
     display_name: Optional[pydantic.StrictStr]
@@ -79,10 +78,7 @@ class CharmMetadataLegacy(
             obj["maintainers"] = [obj["maintainer"]]
             del obj["maintainer"]
 
-        try:
-            return cls.parse_obj(obj)
-        except pydantic.error_wrappers.ValidationError as error:
-            raise CraftError(format_pydantic_errors(error.errors(), file_name=METADATA_FILENAME))
+        return cls.parse_obj(obj)
 
 
 class BundleMetadataLegacy(
@@ -110,7 +106,4 @@ class BundleMetadataLegacy(
 
         :raises CraftError: On failure to unmarshal object.
         """
-        try:
-            return cls.parse_obj(obj)
-        except pydantic.error_wrappers.ValidationError as error:
-            raise CraftError(format_pydantic_errors(error.errors(), file_name=METADATA_FILENAME))
+        return cls.parse_obj(obj)


Test patch:
diff --git a/tests/spread/smoketests/metafiles-bad-yaml-unenforced/task.yaml b/tests/spread/smoketests/metafiles-bad-yaml-unenforced/task.yaml
new file mode 100644
index 00000000..0644cd60
--- /dev/null
+++ b/tests/spread/smoketests/metafiles-bad-yaml-unenforced/task.yaml
@@ -0,0 +1,98 @@
+summary: >
+  create a charm that use charmcraft.yaml, but bad
+  metadata.yaml, config.yaml, actions.yaml
+
+include: 
+  - tests/
+
+prepare: |
+  tests.pkgs install unzip
+  charmcraft init --project-dir=charm
+  rm -f charm/metadata.yaml
+  rm -f charm/config.yaml
+
+  cat <<- EOF > charm/charmcraft.yaml
+  type: charm
+
+  bases:
+    - build-on:
+      - name: ubuntu
+        channel: "22.04"
+      run-on:
+      - name: ubuntu
+        channel: "22.04"
+
+  EOF
+
+  cat <<- EOF > charm/metadata.yaml
+  name: charmcraft-test
+  summary: test-summary
+  description: test-description
+
+  title: test-title
+
+  links:
+    issues: https://example.com/issues
+    contact:
+      - https://example.com/contact
+      - contact@example.com
+      - "IRC #example"
+    documentation: https://example.com/docs
+    source:
+      - https://example.com/source
+      - https://example.com/source2
+      - https://example.com/source3
+    website:
+      - https://example.com/
+
+  meow: meow
+
+  #### TEST-COPY ####
+  EOF
+
+  cat <<- EOF > charm/actions.yaml
+  pause
+  #### TEST-COPY ####
+
+  EOF
+
+  cat <<- EOF > charm/config.yaml
+  options:
+    max-body-size:
+      default: 20
+      description: Max allowed body-size (for file uploads) in megabytes,
+        set to 0 to disable limits.
+      source: default
+      type: int
+      value: 20
+  #### TEST-COPY ####
+
+  EOF
+
+restore: |
+  pushd charm
+  charmcraft clean
+  popd
+  
+  rm -rf charm
+
+execute: |
+  cd charm
+  charmcraft pack --verbose
+  test -f charm*.charm
+
+  unzip -l charm*.charm | MATCH "actions.yaml"
+  unzip -l charm*.charm | MATCH "config.yaml"
+  unzip -l charm*.charm | MATCH "metadata.yaml"
+
+  unzip -p charm*.charm actions.yaml | MATCH "pause"
+  unzip -p charm*.charm actions.yaml | MATCH "#### TEST-COPY ####"
+
+  unzip -p charm*.charm config.yaml | MATCH "max-body-size"
+  unzip -p charm*.charm config.yaml | MATCH "#### TEST-COPY ####"
+
+  unzip -p charm*.charm metadata.yaml | MATCH "meow"
+  unzip -p charm*.charm metadata.yaml | MATCH "#### TEST-COPY ####"
+
+  test ! -d build
+
diff --git a/tests/spread/smoketests/metafiles/combine_yaml/task.yaml b/tests/spread/smoketests/metafiles-multiple-yaml/task.yaml
similarity index 84%
rename from tests/spread/smoketests/metafiles/combine_yaml/task.yaml
rename to tests/spread/smoketests/metafiles-multiple-yaml/task.yaml
index 6e639086..b5a3c141 100644
--- a/tests/spread/smoketests/metafiles/combine_yaml/task.yaml
+++ b/tests/spread/smoketests/metafiles-multiple-yaml/task.yaml
@@ -18,14 +18,14 @@ prepare: |
     - build-on:
       - name: ubuntu
         channel: "22.04"
-    run-on:
+      run-on:
       - name: ubuntu
         channel: "22.04"
 
   EOF
 
   cat <<- EOF > charm/metadata.yaml
-  name: test-charm-name
+  name: charmcraft-test
   summary: test-summary
   description: test-description
 
@@ -112,14 +112,14 @@ execute: |
   unzip -l charm*.charm | MATCH "config.yaml"
   unzip -l charm*.charm | MATCH "metadata.yaml"
 
-  unzip -p actions.yaml charm*.charm | MATCH "description: The type of compression to use."
-  unzip -p actions.yaml charm*.charm | MATCH "#### TEST-COPY ####"
+  unzip -p charm*.charm actions.yaml | MATCH "description: The type of compression to use."
+  unzip -p charm*.charm actions.yaml | MATCH "#### TEST-COPY ####"
 
-  unzip -p config.yaml charm*.charm | MATCH "description: test-2"
-  unzip -p config.yaml charm*.charm | MATCH "#### TEST-COPY ####"
+  unzip -p charm*.charm config.yaml | MATCH "description: test-2"
+  unzip -p charm*.charm config.yaml | MATCH "#### TEST-COPY ####"
 
-  unzip -p metadata.yaml charm*.charm | MATCH "issues: https://example.com/issues"
-  unzip -p metadata.yaml charm*.charm | MATCH "#### TEST-COPY ####"
+  unzip -p charm*.charm metadata.yaml | MATCH "issues: https://example.com/issues"
+  unzip -p charm*.charm metadata.yaml | MATCH "#### TEST-COPY ####"
 
   test ! -d build
 
diff --git a/tests/spread/smoketests/metafiles/charmcraft_yaml/task.yaml b/tests/spread/smoketests/metafiles-unified-charmcraft-yaml/task.yaml
similarity index 78%
rename from tests/spread/smoketests/metafiles/charmcraft_yaml/task.yaml
rename to tests/spread/smoketests/metafiles-unified-charmcraft-yaml/task.yaml
index 6902ccde..a6dd2d33 100644
--- a/tests/spread/smoketests/metafiles/charmcraft_yaml/task.yaml
+++ b/tests/spread/smoketests/metafiles-unified-charmcraft-yaml/task.yaml
@@ -10,7 +10,7 @@ prepare: |
   rm -f charm/config.yaml
 
   cat <<- EOF > charm/charmcraft.yaml
-  name: test-charm-name
+  name: charmcraft-test
   type: charm
   summary: test-summary
   description: test-description
@@ -19,7 +19,7 @@ prepare: |
     - build-on:
       - name: ubuntu
         channel: "22.04"
-    run-on:
+      run-on:
       - name: ubuntu
         channel: "22.04"
 
@@ -65,20 +65,21 @@ prepare: |
       required: [filename]
       additionalProperties: false
 
-  options:
-    test-int:
-      default: 123
-      description: test-1
-      type: int
-    test-string:
-      description: test-2
-      type: string
-    test-float:
-      default: 1.23
-      type: float
-    test-bool:
-      default: true
-      type: boolean
+  config:
+    options:
+      test-int:
+        default: 123
+        description: test-1
+        type: int
+      test-string:
+        description: test-2
+        type: string
+      test-float:
+        default: 1.23
+        type: float
+      test-bool:
+        default: true
+        type: boolean
 
   EOF
 
@@ -96,7 +97,7 @@ execute: |
   unzip -l charm*.charm | MATCH "actions.yaml"
   unzip -l charm*.charm | MATCH "config.yaml"
   unzip -l charm*.charm | MATCH "metadata.yaml"
-  unzip -p actions.yaml charm*.charm | MATCH "description: The type of compression to use."
-  unzip -p config.yaml charm*.charm | MATCH "description: test-2"
-  unzip -p metadata.yaml charm*.charm | MATCH "issues: https://example.com/issues"
+  unzip -p charm*.charm actions.yaml | MATCH "description: The type of compression to use."
+  unzip -p charm*.charm config.yaml | MATCH "description: test-2"
+  unzip -p charm*.charm metadata.yaml | MATCH "issues: https://example.com/issues"
   test ! -d build
diff --git a/tests/test_config.py b/tests/test_config.py
index 83deac1d..577a8849 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -3312,3 +3312,63 @@ def test_actions_defined_in_charmcraft_yaml_and_actions_yaml(
         Bad charmcraft.yaml content:
         - 'actions.yaml' file not allowed when an 'actions' section is defined in 'charmcraft.yaml' in field 'actions'"""  # NOQA: E501
     )
+
+
+@pytest.mark.parametrize(
+    "charmcraft_yaml, metadata_yaml",
+    [
+        [
+            dedent(
+                """\
+                type: charm
+                bases:
+                  - name: test-name
+                    channel: test-channel
+                """
+            ),
+            dedent(
+                """\
+                name: test-charm-name-from-metadata-yaml
+                summary: test summary
+                description: test description
+                """
+            ),
+        ],
+        [
+            dedent(
+                """\
+                type: charm
+                name: test-charm-name-from-charmcraft-yaml
+                summary: test summary
+                description: test description
+                bases:
+                  - name: test-name
+                    channel: test-channel
+                """
+            ),
+            None,
+        ],
+    ],
+)
+def test_actions_bad_unenforced_defined_in_actions_yaml(
+    tmp_path,
+    create_checker,
+    prepare_charmcraft_yaml,
+    prepare_metadata_yaml,
+    prepare_actions_yaml,
+    charmcraft_yaml,
+    metadata_yaml,
+):
+    """Load a bad actions in actions.yaml. Should not raise an error since check unenforced."""
+    prepare_charmcraft_yaml(charmcraft_yaml)
+    prepare_metadata_yaml(metadata_yaml)
+    prepare_actions_yaml(
+        dedent(
+            """\
+            invalid: 111
+            """
+        )
+    )
+
+    config = load(tmp_path)
+    assert config.actions is None
diff --git a/tests/test_metadata.py b/tests/test_metadata.py
index 85b64464..838cf965 100644
--- a/tests/test_metadata.py
+++ b/tests/test_metadata.py
@@ -18,6 +18,7 @@ import re
 from textwrap import dedent
 
 import pytest
+import pydantic
 from craft_cli import CraftError
 
 from charmcraft.metafiles.metadata import parse_charm_metadata_yaml, read_metadata_yaml
@@ -85,15 +86,9 @@ def test_parse_metadata_yaml_error_invalid_names(
 ):
     prepare_metadata_yaml(metadata_yaml_template.format(name=name))
 
-    with pytest.raises(CraftError) as cm:
+    with pytest.raises(pydantic.ValidationError):
         parse_charm_metadata_yaml(tmp_path)
 
-    assert str(cm.value) == dedent(
-        """\
-        Bad metadata.yaml content:
-        - string type expected in field 'name'"""
-    )
-
 
 def test_parse_metadata_yaml_error_missing(tmp_path):
     msg = re.escape(
diff --git a/tests/test_metafiles.py b/tests/test_metafiles.py
index 9a5bece6..a06a6542 100644
--- a/tests/test_metafiles.py
+++ b/tests/test_metafiles.py
@@ -45,6 +45,12 @@ def test_dump_metadata_from_charmcraft_yaml(tmp_path, prepare_charmcraft_yaml):
               - any-of:
                   - extra-feature-1
                   - extra-feature-2
+                  - all-of:
+                    - juju >= 2.9.44
+                    - juju < 3
+                  - all-of:
+                    - juju >= 3.1.6
+                    - juju < 4
               - all-of:
                   - test-feature-1
                   - test-feature-2
@@ -157,7 +163,14 @@ def test_dump_metadata_from_charmcraft_yaml(tmp_path, prepare_charmcraft_yaml):
         "description": "test-description",
         "assumes": [
             "test-feature",
-            {"any-of": ["extra-feature-1", "extra-feature-2"]},
+            {
+                "any-of": [
+                    "extra-feature-1",
+                    "extra-feature-2",
+                    {"all-of": ["juju >= 2.9.44", "juju < 3"]},
+                    {"all-of": ["juju >= 3.1.6", "juju < 4"]},
+                ]
+            },
             {"all-of": ["test-feature-1", "test-feature-2"]},
         ],
         "containers": {
@@ -243,6 +256,12 @@ def test_copy_metadata_from_metadata_yaml(
               - any-of:
                   - extra-feature-1
                   - extra-feature-2
+                  - all-of:
+                    - juju >= 2.9.44
+                    - juju < 3
+                  - all-of:
+                    - juju >= 3.1.6
+                    - juju < 4
               - all-of:
                   - test-feature-1
                   - test-feature-2
@@ -367,7 +386,14 @@ def test_copy_metadata_from_metadata_yaml(
         "description": "test-description",
         "assumes": [
             "test-feature",
-            {"any-of": ["extra-feature-1", "extra-feature-2"]},
+            {
+                "any-of": [
+                    "extra-feature-1",
+                    "extra-feature-2",
+                    {"all-of": ["juju >= 2.9.44", "juju < 3"]},
+                    {"all-of": ["juju >= 3.1.6", "juju < 4"]},
+                ]
+            },
             {"all-of": ["test-feature-1", "test-feature-2"]},
         ],
         "containers": {
diff --git a/tests/test_models.py b/tests/test_models.py
index 127bd9de..d74349e2 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -301,6 +301,39 @@ def test_load_minimal_metadata_from_metadata_yaml_missing_summary(
         load(tmp_path)
 
 
+def test_load_minimal_metadata_from_metadata_yaml_bad_others_allowed(
+    tmp_path, prepare_charmcraft_yaml, prepare_metadata_yaml
+):
+    """Load a mimimal charmcraft.yaml with metadata.yaml. Had bad other fields but allowed."""
+    prepare_charmcraft_yaml(
+        dedent(
+            """
+            type: charm
+
+            bases:
+                - name: test-name
+                  channel: test-channel
+            """
+        ),
+    )
+    prepare_metadata_yaml(
+        dedent(
+            """
+            name: test-charm-name
+            summary: test-summary
+            description: test-description
+
+            peers:
+              - aaa
+              - bbb
+              - ccc
+            """
+        ),
+    )
+
+    load(tmp_path)
+
+
 def test_load_minimal_metadata_from_metadata_yaml_missing_description(
     tmp_path, prepare_charmcraft_yaml, prepare_metadata_yaml
 ):
@@ -1161,6 +1194,31 @@ def test_load_config_in_config_yaml(tmp_path, prepare_charmcraft_yaml, prepare_c
     }
 
 
+def test_load_bad_config_in_config_yaml(tmp_path, prepare_charmcraft_yaml, prepare_config_yaml):
+    """Load a bad config in config.yaml. Should not raise an error since check unenforced."""
+    prepare_charmcraft_yaml(
+        dedent(
+            """
+            name: test-charm-name
+            type: charm
+            summary: test-summary
+            description: test-description
+            """
+        ),
+    )
+    prepare_config_yaml(
+        dedent(
+            """
+            options:
+              test-int:
+            """
+        ),
+    )
+    config = load(tmp_path)
+
+    assert config.config is None
+
+
 def test_load_bad_config_in_charmcraft_yaml(tmp_path, prepare_charmcraft_yaml):
     """Load a config in charmcraft.yaml."""
     prepare_charmcraft_yaml(