From 29767b78733b75f411ab91bf8bad59538b408cbd Mon Sep 17 00:00:00 2001 From: Luis Orofino Date: Fri, 8 May 2026 15:28:49 +0200 Subject: [PATCH 1/3] Add ddev validate --- ddev/src/ddev/ai/tools/shell/ddev/validate.py | 67 +++++++++++++++++++ .../ai/tools/shell/ddev/test_ddev_tools.py | 24 +++++++ 2 files changed, 91 insertions(+) create mode 100644 ddev/src/ddev/ai/tools/shell/ddev/validate.py diff --git a/ddev/src/ddev/ai/tools/shell/ddev/validate.py b/ddev/src/ddev/ai/tools/shell/ddev/validate.py new file mode 100644 index 0000000000000..8e8e51a459319 --- /dev/null +++ b/ddev/src/ddev/ai/tools/shell/ddev/validate.py @@ -0,0 +1,67 @@ +# (C) Datadog, Inc. 2026-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) +from typing import Annotated, Literal + +from pydantic import Field + +from ddev.ai.tools.core.base import BaseToolInput +from ddev.ai.tools.shell.base import CmdTool + + +class DdevValidateInput(BaseToolInput): + subcommand: Annotated[ + Literal["config", "models", "metadata", "all"], + Field( + description=( + "Which validator to run. " + "'config' validates assets/configuration/spec.yaml against data/conf.yaml.example. " + "'models' validates spec.yaml against datadog_checks//config_models/. " + "'metadata' validates metadata.csv. " + "'all' runs the full orchestrator across every validator." + ) + ), + ] + integration: Annotated[str, Field(description="Integration name to validate")] + sync: Annotated[ + bool, + Field( + description=( + "Regenerate / auto-fix derived files instead of only checking. " + "For 'config' and 'models', regenerates conf.yaml.example and config_models/ from spec.yaml. " + "For 'metadata', rewrites metadata.csv into canonical form. " + "For 'all', forwards an auto-fix flag to every sub-validator that supports it." + ) + ), + ] = False + + +# Maps the user-facing `sync` boolean to the actual CLI flag the underlying +# `ddev validate ` accepts. +_SYNC_FLAG: dict[str, str] = { + "config": "-s", + "models": "-s", + "metadata": "--sync", + "all": "--fix", +} + + +class DdevValidateTool(CmdTool[DdevValidateInput]): + """Validates an integration's spec, generated config example, generated + Pydantic config models, or metadata.csv. Set `sync=true` to regenerate the + derived files (conf.yaml.example, config_models/, metadata.csv) from the + spec, instead of only checking them. Always run with `sync=true` after + editing assets/configuration/spec.yaml so the generated files stay in sync.""" + + timeout = 120 + + @property + def name(self) -> str: + return "ddev_validate" + + def cmd(self, tool_input: DdevValidateInput) -> list[str]: + cmd = ["ddev", "--no-interactive", "validate", tool_input.subcommand] + if tool_input.sync: + cmd.append(_SYNC_FLAG[tool_input.subcommand]) + cmd.append(tool_input.integration) + return cmd diff --git a/ddev/tests/ai/tools/shell/ddev/test_ddev_tools.py b/ddev/tests/ai/tools/shell/ddev/test_ddev_tools.py index cc4eb72a7e182..4ee0b8d87db06 100644 --- a/ddev/tests/ai/tools/shell/ddev/test_ddev_tools.py +++ b/ddev/tests/ai/tools/shell/ddev/test_ddev_tools.py @@ -11,6 +11,7 @@ from ddev.ai.tools.shell.ddev.env_stop import DdevEnvStopTool, EnvStopInput from ddev.ai.tools.shell.ddev.env_test import DdevEnvTestTool, EnvTestInput from ddev.ai.tools.shell.ddev.release_changelog import DdevReleaseChangelogTool, ReleaseChangelogInput +from ddev.ai.tools.shell.ddev.validate import DdevValidateInput, DdevValidateTool # --- ddev create --- @@ -163,3 +164,26 @@ def test_release_changelog_cmd_message_placement(): def test_release_changelog_invalid_change_type_raises(): with pytest.raises(ValidationError): ReleaseChangelogInput(change_type="patch", integration="mycheck", message="Some message") + + +# --- ddev validate --- + + +@pytest.mark.parametrize("subcommand", ["config", "models", "metadata", "all"]) +def test_validate_cmd_all_subcommands(subcommand: str): + cmd = DdevValidateTool().cmd(DdevValidateInput(subcommand=subcommand, integration="mycheck")) + assert cmd == ["ddev", "--no-interactive", "validate", subcommand, "mycheck"] + + +@pytest.mark.parametrize( + "subcommand,expected_flag", + [ + ("config", "-s"), + ("models", "-s"), + ("metadata", "--sync"), + ("all", "--fix"), + ], +) +def test_validate_cmd_sync_flag_per_subcommand(subcommand: str, expected_flag: str): + cmd = DdevValidateTool().cmd(DdevValidateInput(subcommand=subcommand, integration="mycheck", sync=True)) + assert cmd[4] == expected_flag From 7ddb43c45cbe7e862b55f91fa7db61ec8910f84f Mon Sep 17 00:00:00 2001 From: Luis Orofino Date: Fri, 8 May 2026 16:29:46 +0200 Subject: [PATCH 2/3] Drop ddev validate all and fix some bugs --- ddev/src/ddev/ai/tools/registry.py | 1 + ddev/src/ddev/ai/tools/shell/ddev/validate.py | 21 +++++++------------ .../ai/tools/shell/ddev/test_ddev_tools.py | 10 ++++++--- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/ddev/src/ddev/ai/tools/registry.py b/ddev/src/ddev/ai/tools/registry.py index f8249bfb7fd93..3fd255f7bda1b 100644 --- a/ddev/src/ddev/ai/tools/registry.py +++ b/ddev/src/ddev/ai/tools/registry.py @@ -70,6 +70,7 @@ class ToolSpec: "ddev_env_stop": ToolSpec("shell.ddev.env_stop", "DdevEnvStopTool"), "ddev_env_test": ToolSpec("shell.ddev.env_test", "DdevEnvTestTool"), "ddev_release_changelog": ToolSpec("shell.ddev.release_changelog", "DdevReleaseChangelogTool"), + "ddev_validate": ToolSpec("shell.ddev.validate", "DdevValidateTool"), } diff --git a/ddev/src/ddev/ai/tools/shell/ddev/validate.py b/ddev/src/ddev/ai/tools/shell/ddev/validate.py index 8e8e51a459319..23c76f7368562 100644 --- a/ddev/src/ddev/ai/tools/shell/ddev/validate.py +++ b/ddev/src/ddev/ai/tools/shell/ddev/validate.py @@ -8,17 +8,18 @@ from ddev.ai.tools.core.base import BaseToolInput from ddev.ai.tools.shell.base import CmdTool +ValidateSubcommand = Literal["config", "models", "metadata"] + class DdevValidateInput(BaseToolInput): subcommand: Annotated[ - Literal["config", "models", "metadata", "all"], + ValidateSubcommand, Field( description=( "Which validator to run. " "'config' validates assets/configuration/spec.yaml against data/conf.yaml.example. " "'models' validates spec.yaml against datadog_checks//config_models/. " "'metadata' validates metadata.csv. " - "'all' runs the full orchestrator across every validator." ) ), ] @@ -28,30 +29,24 @@ class DdevValidateInput(BaseToolInput): Field( description=( "Regenerate / auto-fix derived files instead of only checking. " - "For 'config' and 'models', regenerates conf.yaml.example and config_models/ from spec.yaml. " + "For 'config', regenerates conf.yaml.example. " + "For 'models', regenerates config_models/. " "For 'metadata', rewrites metadata.csv into canonical form. " - "For 'all', forwards an auto-fix flag to every sub-validator that supports it." ) ), ] = False -# Maps the user-facing `sync` boolean to the actual CLI flag the underlying -# `ddev validate ` accepts. -_SYNC_FLAG: dict[str, str] = { +_SYNC_FLAG: dict[ValidateSubcommand, str] = { "config": "-s", "models": "-s", "metadata": "--sync", - "all": "--fix", } class DdevValidateTool(CmdTool[DdevValidateInput]): - """Validates an integration's spec, generated config example, generated - Pydantic config models, or metadata.csv. Set `sync=true` to regenerate the - derived files (conf.yaml.example, config_models/, metadata.csv) from the - spec, instead of only checking them. Always run with `sync=true` after - editing assets/configuration/spec.yaml so the generated files stay in sync.""" + """Validates an integration's spec, config example, config models, or metadata.csv. + Set `sync=true` to regenerate the derived files from spec.yaml.""" timeout = 120 diff --git a/ddev/tests/ai/tools/shell/ddev/test_ddev_tools.py b/ddev/tests/ai/tools/shell/ddev/test_ddev_tools.py index 4ee0b8d87db06..dd47993261748 100644 --- a/ddev/tests/ai/tools/shell/ddev/test_ddev_tools.py +++ b/ddev/tests/ai/tools/shell/ddev/test_ddev_tools.py @@ -169,7 +169,7 @@ def test_release_changelog_invalid_change_type_raises(): # --- ddev validate --- -@pytest.mark.parametrize("subcommand", ["config", "models", "metadata", "all"]) +@pytest.mark.parametrize("subcommand", ["config", "models", "metadata"]) def test_validate_cmd_all_subcommands(subcommand: str): cmd = DdevValidateTool().cmd(DdevValidateInput(subcommand=subcommand, integration="mycheck")) assert cmd == ["ddev", "--no-interactive", "validate", subcommand, "mycheck"] @@ -181,9 +181,13 @@ def test_validate_cmd_all_subcommands(subcommand: str): ("config", "-s"), ("models", "-s"), ("metadata", "--sync"), - ("all", "--fix"), ], ) def test_validate_cmd_sync_flag_per_subcommand(subcommand: str, expected_flag: str): cmd = DdevValidateTool().cmd(DdevValidateInput(subcommand=subcommand, integration="mycheck", sync=True)) - assert cmd[4] == expected_flag + assert cmd == ["ddev", "--no-interactive", "validate", subcommand, expected_flag, "mycheck"] + + +def test_validate_invalid_subcommand_raises(): + with pytest.raises(ValidationError): + DdevValidateInput(subcommand="lint", integration="mycheck") From 29d959a337aefac34bf73ef2bdea92d5f7d90d9c Mon Sep 17 00:00:00 2001 From: Luis Orofino Date: Fri, 8 May 2026 16:43:33 +0200 Subject: [PATCH 3/3] Use --sync for every subcommand --- ddev/src/ddev/ai/tools/shell/ddev/validate.py | 9 +-------- ddev/tests/ai/tools/shell/ddev/test_ddev_tools.py | 13 +++---------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/ddev/src/ddev/ai/tools/shell/ddev/validate.py b/ddev/src/ddev/ai/tools/shell/ddev/validate.py index 23c76f7368562..0f49834fdf384 100644 --- a/ddev/src/ddev/ai/tools/shell/ddev/validate.py +++ b/ddev/src/ddev/ai/tools/shell/ddev/validate.py @@ -37,13 +37,6 @@ class DdevValidateInput(BaseToolInput): ] = False -_SYNC_FLAG: dict[ValidateSubcommand, str] = { - "config": "-s", - "models": "-s", - "metadata": "--sync", -} - - class DdevValidateTool(CmdTool[DdevValidateInput]): """Validates an integration's spec, config example, config models, or metadata.csv. Set `sync=true` to regenerate the derived files from spec.yaml.""" @@ -57,6 +50,6 @@ def name(self) -> str: def cmd(self, tool_input: DdevValidateInput) -> list[str]: cmd = ["ddev", "--no-interactive", "validate", tool_input.subcommand] if tool_input.sync: - cmd.append(_SYNC_FLAG[tool_input.subcommand]) + cmd.append("--sync") cmd.append(tool_input.integration) return cmd diff --git a/ddev/tests/ai/tools/shell/ddev/test_ddev_tools.py b/ddev/tests/ai/tools/shell/ddev/test_ddev_tools.py index dd47993261748..0f306e51fe7d6 100644 --- a/ddev/tests/ai/tools/shell/ddev/test_ddev_tools.py +++ b/ddev/tests/ai/tools/shell/ddev/test_ddev_tools.py @@ -175,17 +175,10 @@ def test_validate_cmd_all_subcommands(subcommand: str): assert cmd == ["ddev", "--no-interactive", "validate", subcommand, "mycheck"] -@pytest.mark.parametrize( - "subcommand,expected_flag", - [ - ("config", "-s"), - ("models", "-s"), - ("metadata", "--sync"), - ], -) -def test_validate_cmd_sync_flag_per_subcommand(subcommand: str, expected_flag: str): +@pytest.mark.parametrize("subcommand", ["config", "models", "metadata"]) +def test_validate_cmd_sync_flag_per_subcommand(subcommand: str): cmd = DdevValidateTool().cmd(DdevValidateInput(subcommand=subcommand, integration="mycheck", sync=True)) - assert cmd == ["ddev", "--no-interactive", "validate", subcommand, expected_flag, "mycheck"] + assert cmd == ["ddev", "--no-interactive", "validate", subcommand, "--sync", "mycheck"] def test_validate_invalid_subcommand_raises():