Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ddev/src/ddev/ai/tools/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
}


Expand Down
55 changes: 55 additions & 0 deletions ddev/src/ddev/ai/tools/shell/ddev/validate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# (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

ValidateSubcommand = Literal["config", "models", "metadata"]


class DdevValidateInput(BaseToolInput):
subcommand: Annotated[
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/<integration>/config_models/. "
"'metadata' validates metadata.csv. "
)
),
]
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', regenerates conf.yaml.example. "
"For 'models', regenerates config_models/. "
"For 'metadata', rewrites metadata.csv into canonical form. "
)
),
] = False


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."""

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")
cmd.append(tool_input.integration)
return cmd
21 changes: 21 additions & 0 deletions ddev/tests/ai/tools/shell/ddev/test_ddev_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---

Expand Down Expand Up @@ -163,3 +164,23 @@ 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"])
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", ["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, "--sync", "mycheck"]


def test_validate_invalid_subcommand_raises():
with pytest.raises(ValidationError):
DdevValidateInput(subcommand="lint", integration="mycheck")
Loading