Skip to content

Commit e5e4d72

Browse files
committed
Add ddev validate to ddev tools (#23636)
* Add ddev validate * Drop ddev validate all and fix some bugs * Use --sync for every subcommand * Add ddev validate all again
1 parent 46cf0ae commit e5e4d72

3 files changed

Lines changed: 88 additions & 0 deletions

File tree

ddev/src/ddev/ai/tools/registry.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ class ToolSpec:
7070
"ddev_env_stop": ToolSpec("shell.ddev.env_stop", "DdevEnvStopTool"),
7171
"ddev_env_test": ToolSpec("shell.ddev.env_test", "DdevEnvTestTool"),
7272
"ddev_release_changelog": ToolSpec("shell.ddev.release_changelog", "DdevReleaseChangelogTool"),
73+
"ddev_validate": ToolSpec("shell.ddev.validate", "DdevValidateTool"),
7374
}
7475

7576

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# (C) Datadog, Inc. 2026-present
2+
# All rights reserved
3+
# Licensed under a 3-clause BSD style license (see LICENSE)
4+
from typing import Annotated, Literal
5+
6+
from pydantic import Field
7+
8+
from ddev.ai.tools.core.base import BaseToolInput
9+
from ddev.ai.tools.shell.base import CmdTool
10+
11+
ValidateSubcommand = Literal["config", "models", "metadata", "all"]
12+
13+
14+
class DdevValidateInput(BaseToolInput):
15+
subcommand: Annotated[
16+
ValidateSubcommand,
17+
Field(
18+
description=(
19+
"Which validator to run. Options:"
20+
"- 'config': validates assets/configuration/spec.yaml against data/conf.yaml.example. "
21+
"- 'models': validates spec.yaml against datadog_checks/<integration>/config_models/. "
22+
"- 'metadata': validates metadata.csv. "
23+
"- 'all': runs all ~20 validators in parallel. Some of these always scan the entire "
24+
"repository, so the output may include failures for files outside of <integration>. "
25+
"IGNORE those unrelated failures — only act on failures that reference files inside "
26+
"your integration's directory. It might take a long time to run. Use 'all' only as "
27+
"a final sweep to catch issues the targeted validators do not cover."
28+
)
29+
),
30+
]
31+
integration: Annotated[str, Field(description="Integration name to validate")]
32+
sync: Annotated[
33+
bool,
34+
Field(
35+
description=(
36+
"Regenerate / auto-fix derived files instead of only checking. "
37+
"For 'config', regenerates conf.yaml.example. "
38+
"For 'models', regenerates config_models/. "
39+
"For 'metadata', rewrites metadata.csv into canonical form. "
40+
"For 'all', auto-fixes every validator that supports it."
41+
)
42+
),
43+
] = False
44+
45+
46+
class DdevValidateTool(CmdTool[DdevValidateInput]):
47+
"""Validates an integration's spec, config example, config models, or metadata.csv.
48+
Set `sync=true` to regenerate the derived files from spec.yaml."""
49+
50+
timeout = 660
51+
52+
@property
53+
def name(self) -> str:
54+
return "ddev_validate"
55+
56+
def cmd(self, tool_input: DdevValidateInput) -> list[str]:
57+
cmd = ["ddev", "--no-interactive", "validate", tool_input.subcommand]
58+
if tool_input.sync:
59+
cmd.append("--fix" if tool_input.subcommand == "all" else "--sync")
60+
cmd.append(tool_input.integration)
61+
return cmd

ddev/tests/ai/tools/shell/ddev/test_ddev_tools.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from ddev.ai.tools.shell.ddev.env_stop import DdevEnvStopTool, EnvStopInput
1212
from ddev.ai.tools.shell.ddev.env_test import DdevEnvTestTool, EnvTestInput
1313
from ddev.ai.tools.shell.ddev.release_changelog import DdevReleaseChangelogTool, ReleaseChangelogInput
14+
from ddev.ai.tools.shell.ddev.validate import DdevValidateInput, DdevValidateTool
1415

1516
# --- ddev create ---
1617

@@ -163,3 +164,28 @@ def test_release_changelog_cmd_message_placement():
163164
def test_release_changelog_invalid_change_type_raises():
164165
with pytest.raises(ValidationError):
165166
ReleaseChangelogInput(change_type="patch", integration="mycheck", message="Some message")
167+
168+
169+
# --- ddev validate ---
170+
171+
172+
@pytest.mark.parametrize("subcommand", ["config", "models", "metadata", "all"])
173+
def test_validate_cmd_all_subcommands(subcommand: str):
174+
cmd = DdevValidateTool().cmd(DdevValidateInput(subcommand=subcommand, integration="mycheck"))
175+
assert cmd == ["ddev", "--no-interactive", "validate", subcommand, "mycheck"]
176+
177+
178+
@pytest.mark.parametrize("subcommand", ["config", "models", "metadata"])
179+
def test_validate_cmd_sync_flag_per_subcommand(subcommand: str):
180+
cmd = DdevValidateTool().cmd(DdevValidateInput(subcommand=subcommand, integration="mycheck", sync=True))
181+
assert cmd == ["ddev", "--no-interactive", "validate", subcommand, "--sync", "mycheck"]
182+
183+
184+
def test_validate_cmd_all_uses_fix_flag():
185+
cmd = DdevValidateTool().cmd(DdevValidateInput(subcommand="all", integration="mycheck", sync=True))
186+
assert cmd == ["ddev", "--no-interactive", "validate", "all", "--fix", "mycheck"]
187+
188+
189+
def test_validate_invalid_subcommand_raises():
190+
with pytest.raises(ValidationError):
191+
DdevValidateInput(subcommand="lint", integration="mycheck")

0 commit comments

Comments
 (0)