Skip to content

Commit 0ef7a10

Browse files
authored
Add descriptions to validations and improve PR comment formatting (DataDog#23308)
* Add descriptions to validations and improve PR comment formatting - Add a description field to ValidationConfig shown in summary tables - Collapse the validation table in PR comments: all-pass wraps the entire table in a <details> block; partial failures show only failures up front with passed validations collapsed - Keep the GitHub Actions step summary as a full flat table - Remove redundant "View full run" link from PR comments * Keep run URL in PR comment, only omit from step summary * Add changelog entry * Address review feedback: extract _build_preamble helper, add missing test coverage, minor cleanups * Rename _post_pr_comment to _publish_report, single-pass partition of results, use extend over repeated appends * Unify section builders into a single _build_report_section with collapsed flag * Fix changelog entry number * Warn about incomplete validations in PR comment and step summary * Use descriptive variable names in results loop * Make expected_count a required positional parameter * Show names of incomplete validations instead of just the count * Lint formatting * Fix Windows test failure: read summary file with utf-8 encoding
1 parent 3d386c3 commit 0ef7a10

5 files changed

Lines changed: 384 additions & 90 deletions

File tree

ddev/changelog.d/23308.fixed

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add descriptions to validations and improve PR comment formatting with collapsible sections

ddev/src/ddev/cli/validate/all/github.py

Lines changed: 97 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
if TYPE_CHECKING:
1414
from ddev.cli.application import Application
15-
from ddev.cli.validate.all.orchestrator import ValidationResult
15+
from ddev.cli.validate.all.orchestrator import ValidationConfig, ValidationResult
1616

1717
COMMENT_HEADING = "## Validation Report"
1818

@@ -75,27 +75,112 @@ def write_step_summary(content: str) -> None:
7575
f.write(content + "\n")
7676

7777

78+
def _build_preamble(error: str | None, warning: str | None) -> list[str]:
79+
parts: list[str] = [f"{COMMENT_HEADING}\n"]
80+
if error:
81+
parts.append(f"> **Error:** {error}\n")
82+
if warning:
83+
parts.append(f"> **Warning:** {warning}\n")
84+
return parts
85+
86+
87+
def _build_table(
88+
rows: dict[str, ValidationResult],
89+
configs: dict[str, ValidationConfig],
90+
) -> list[str]:
91+
"""Build a markdown table with Validation, Description, and Status columns."""
92+
from ddev.cli.validate.all.orchestrator import ValidationConfig as _VC
93+
94+
lines = ["| Validation | Description | Status |", "|---|---|---|"]
95+
for name in sorted(rows):
96+
status = "✅" if rows[name].success else "❌"
97+
description = configs.get(name, _VC()).description
98+
lines.append(f"| `{name}` | {description} | {status} |")
99+
return lines
100+
101+
102+
def _build_report_section(
103+
rows: dict[str, ValidationResult],
104+
configs: dict[str, ValidationConfig],
105+
*,
106+
header: str | None = None,
107+
collapsed: bool = False,
108+
) -> list[str]:
109+
table = _build_table(rows, configs)
110+
if collapsed:
111+
summary = header or "Details"
112+
return [
113+
"",
114+
f"<details>\n<summary>{summary}</summary>\n",
115+
*table,
116+
"\n</details>",
117+
]
118+
if header:
119+
return [header, "", *table]
120+
return table
121+
122+
123+
def _build_incomplete_warning(expected_validations: list[str], results: dict[str, ValidationResult]) -> list[str]:
124+
missing = sorted(set(expected_validations) - set(results))
125+
if not missing:
126+
return []
127+
names = ", ".join(f"`{n}`" for n in missing)
128+
return [f"> **Warning:** {len(missing)} validation(s) did not complete: {names}\n"]
129+
130+
78131
def format_pr_comment(
79132
results: dict[str, ValidationResult],
133+
configs: dict[str, ValidationConfig],
80134
target: str | None,
135+
expected_validations: list[str],
81136
*,
82137
error: str | None = None,
83138
warning: str | None = None,
84139
) -> str:
85-
failures = {n for n, r in results.items() if not r.success}
86-
87-
parts = [f"{COMMENT_HEADING}\n"]
88-
if error:
89-
parts.append(f"> **Error:** {error}\n")
90-
if warning:
91-
parts.append(f"> **Warning:** {warning}\n")
140+
"""Format a PR comment with collapsible sections to reduce clutter."""
141+
failures: dict[str, ValidationResult] = {}
142+
passed: dict[str, ValidationResult] = {}
143+
for name, result in results.items():
144+
(passed if result.success else failures)[name] = result
92145

93-
parts.extend(("| Validation | Status |", "|---|---|"))
94-
for name in sorted(results):
95-
status = "❌" if name in failures else "✅"
96-
parts.append(f"| `{name}` | {status} |")
146+
incomplete = _build_incomplete_warning(expected_validations, results)
147+
parts = _build_preamble(error, warning)
148+
parts.extend(incomplete)
97149

98150
if failures:
151+
parts.extend(_build_report_section(failures, configs))
152+
fix_target = f" {target}" if target else ""
153+
parts.append(f"\nRun `ddev validate all{fix_target} --fix` to attempt to auto-fix supported validations.")
154+
155+
if passed:
156+
if failures or incomplete:
157+
header = f"Passed validations ({len(passed)})"
158+
else:
159+
parts.append(f"All {len(passed)} validations passed.")
160+
header = "Show details"
161+
parts.extend(_build_report_section(passed, configs, header=header, collapsed=True))
162+
163+
return "\n".join(parts)
164+
165+
166+
def format_step_summary(
167+
results: dict[str, ValidationResult],
168+
configs: dict[str, ValidationConfig],
169+
target: str | None,
170+
expected_validations: list[str],
171+
*,
172+
error: str | None = None,
173+
warning: str | None = None,
174+
) -> str:
175+
"""Format a flat summary table for the GitHub Actions step summary."""
176+
has_failures = any(not r.success for r in results.values())
177+
178+
parts = _build_preamble(error, warning)
179+
parts.extend(_build_incomplete_warning(expected_validations, results))
180+
181+
parts.extend(_build_table(results, configs))
182+
183+
if has_failures:
99184
fix_target = f" {target}" if target else ""
100185
fix_all_cmd = f"ddev validate all{fix_target} --fix"
101186
parts.append(f"\nRun `{fix_all_cmd}` to attempt to auto-fix supported validations.")

ddev/src/ddev/cli/validate/all/orchestrator.py

Lines changed: 103 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from ddev.cli.validate.all.github import (
1717
COMMENT_HEADING,
1818
format_pr_comment,
19+
format_step_summary,
1920
get_workflow_run_url,
2021
write_step_summary,
2122
)
@@ -27,6 +28,7 @@
2728

2829
@dataclass(frozen=True)
2930
class ValidationConfig:
31+
description: str = ""
3032
repo_wide: bool = False
3133
fix_flag: str | None = None
3234

@@ -36,26 +38,78 @@ class ValidationConfig:
3638
# are migrated, we can improve the command structure so each validation
3739
# auto-registers itself instead of maintaining this manually.
3840
VALIDATIONS: dict[str, ValidationConfig] = {
39-
"agent-reqs": ValidationConfig(),
40-
"ci": ValidationConfig(repo_wide=True, fix_flag="--sync"),
41-
"codeowners": ValidationConfig(repo_wide=True),
42-
"config": ValidationConfig(fix_flag="--sync"),
43-
"dep": ValidationConfig(repo_wide=True),
44-
"http": ValidationConfig(),
45-
"imports": ValidationConfig(),
46-
"integration-style": ValidationConfig(),
47-
"jmx-metrics": ValidationConfig(),
48-
"labeler": ValidationConfig(repo_wide=True, fix_flag="--sync"),
49-
"legacy-signature": ValidationConfig(),
50-
"license-headers": ValidationConfig(fix_flag="--fix"),
51-
"licenses": ValidationConfig(repo_wide=True, fix_flag="--sync"),
52-
"metadata": ValidationConfig(fix_flag="--sync"),
53-
"models": ValidationConfig(fix_flag="--sync"),
54-
"openmetrics": ValidationConfig(),
55-
"package": ValidationConfig(),
56-
"readmes": ValidationConfig(),
57-
"saved-views": ValidationConfig(),
58-
"version": ValidationConfig(),
41+
"agent-reqs": ValidationConfig(
42+
description="Verify check versions match the Agent requirements file",
43+
),
44+
"ci": ValidationConfig(
45+
description="Validate CI configuration and Codecov settings",
46+
repo_wide=True,
47+
fix_flag="--sync",
48+
),
49+
"codeowners": ValidationConfig(
50+
description="Validate every integration has a CODEOWNERS entry",
51+
repo_wide=True,
52+
),
53+
"config": ValidationConfig(
54+
description="Validate default configuration files against spec.yaml",
55+
fix_flag="--sync",
56+
),
57+
"dep": ValidationConfig(
58+
description="Verify dependency pins are consistent and Agent-compatible",
59+
repo_wide=True,
60+
),
61+
"http": ValidationConfig(
62+
description="Validate integrations use the HTTP wrapper correctly",
63+
),
64+
"imports": ValidationConfig(
65+
description="Validate check imports do not use deprecated modules",
66+
),
67+
"integration-style": ValidationConfig(
68+
description="Validate check code style conventions",
69+
),
70+
"jmx-metrics": ValidationConfig(
71+
description="Validate JMX metrics definition files and config",
72+
),
73+
"labeler": ValidationConfig(
74+
description="Validate PR labeler config matches integration directories",
75+
repo_wide=True,
76+
fix_flag="--sync",
77+
),
78+
"legacy-signature": ValidationConfig(
79+
description="Validate no integration uses the legacy Agent check signature",
80+
),
81+
"license-headers": ValidationConfig(
82+
description="Validate Python files have proper license headers",
83+
fix_flag="--fix",
84+
),
85+
"licenses": ValidationConfig(
86+
description="Validate third-party license attribution list",
87+
repo_wide=True,
88+
fix_flag="--sync",
89+
),
90+
"metadata": ValidationConfig(
91+
description="Validate metadata.csv metric definitions",
92+
fix_flag="--sync",
93+
),
94+
"models": ValidationConfig(
95+
description="Validate configuration data models match spec.yaml",
96+
fix_flag="--sync",
97+
),
98+
"openmetrics": ValidationConfig(
99+
description="Validate OpenMetrics integrations disable the metric limit",
100+
),
101+
"package": ValidationConfig(
102+
description="Validate Python package metadata and naming",
103+
),
104+
"readmes": ValidationConfig(
105+
description="Validate README files have required sections",
106+
),
107+
"saved-views": ValidationConfig(
108+
description="Validate saved view JSON file structure and fields",
109+
),
110+
"version": ValidationConfig(
111+
description="Validate version consistency between package and changelog",
112+
),
59113
}
60114

61115

@@ -166,21 +220,18 @@ async def on_finalize(self, exception: Exception | None) -> None:
166220
if exception is not None:
167221
self._app.display_error(f"Error running validations: {exception}")
168222

169-
self._post_pr_comment(exception)
223+
self._publish_report(exception)
170224
self._print_console_output()
171225

172-
def _build_report_body(self, exception: Exception | None) -> str:
226+
def _build_error_and_warning(self, exception: Exception | None) -> tuple[str | None, str | None]:
173227
error_msg = f"Error running validations: {exception}" if exception else None
174228

175229
if self._pr_number is None and os.environ.get("GITHUB_EVENT_NAME") == "pull_request":
176230
extra_warning = "Running in pull_request context but could not determine PR number to post a comment."
177231
else:
178232
extra_warning = None
179233

180-
body = format_pr_comment(self._results, self._target, error=error_msg, warning=extra_warning)
181-
if run_url := get_workflow_run_url():
182-
body += f"\n\n[View full run]({run_url})"
183-
return body
234+
return error_msg, extra_warning
184235

185236
def _delete_previous_comments(self, pr_number: int) -> None:
186237
try:
@@ -191,9 +242,29 @@ def _delete_previous_comments(self, pr_number: int) -> None:
191242
except Exception as exc:
192243
self._app.display_warning(f"Failed to clean up previous validation comments: {exc}")
193244

194-
def _post_pr_comment(self, exception: Exception | None) -> None:
195-
body = self._build_report_body(exception)
196-
write_step_summary(body)
245+
def _publish_report(self, exception: Exception | None) -> None:
246+
error_msg, extra_warning = self._build_error_and_warning(exception)
247+
248+
summary_body = format_step_summary(
249+
self._results,
250+
VALIDATIONS,
251+
self._target,
252+
self._validations,
253+
error=error_msg,
254+
warning=extra_warning,
255+
)
256+
write_step_summary(summary_body)
257+
258+
comment_body = format_pr_comment(
259+
self._results,
260+
VALIDATIONS,
261+
self._target,
262+
self._validations,
263+
error=error_msg,
264+
warning=extra_warning,
265+
)
266+
if run_url := get_workflow_run_url():
267+
comment_body += f"\n\n[View full run]({run_url})"
197268

198269
self._app.logger.debug("PR number: %s", self._pr_number)
199270
self._app.logger.debug("GitHub token configured: %s", bool(self._app.config.github.token))
@@ -212,11 +283,11 @@ def _post_pr_comment(self, exception: Exception | None) -> None:
212283
self._app.logger.debug("Deleting previous validation comments on PR #%s...", self._pr_number)
213284
self._delete_previous_comments(self._pr_number)
214285
self._app.logger.debug("Posting validation comment on PR #%s...", self._pr_number)
215-
self._app.github.post_pull_request_comment(self._pr_number, body)
286+
self._app.github.post_pull_request_comment(self._pr_number, comment_body)
216287
self._app.logger.debug("Comment posted successfully.")
217288
except Exception as exc:
218-
self._app.display_warning(f"Failed to post PR comment: {exc}")
219-
write_step_summary(f"\n> Failed to post PR comment: {exc}")
289+
self._app.display_warning(f"Failed to post PR comment: {type(exc).__name__}: {exc}")
290+
write_step_summary(f"\n> Failed to post PR comment: {type(exc).__name__}: {exc}")
220291
finally:
221292
httpx_logger.setLevel(previous_level)
222293

0 commit comments

Comments
 (0)