Files
kleinanzeigen-bot/scripts/check_generated_artifacts.py
Jens eda06334d1 docs: generate README usage section (#1191)
## ℹ️ Description

- Link to the related issue(s): N/A
- Keeps the README Usage command block in sync with the CLI help by
generating it as part of the artifact pipeline.

## 📋 Changes Summary

- Added a README Usage generator that renders normalized English CLI
help between explicit README markers.
- Added `generate-readme-commands` and wired it into
`generate-artifacts`.
- Extended generated artifact checks to detect README Usage drift
without mutating files.
- Added focused unit tests for marker replacement, locale/executable
normalization, ANSI stripping, and command coverage.
- No dependency changes introduced.

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [x]  New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)


##  Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Kept the README “Usage” section automatically synchronized with the
app’s CLI help.
* Added a new `--preserve-local-settings` option to the CLI help output.

* **Bug Fixes**
* Improved and clarified CLI command/option descriptions (notably around
`verify`, `extend`, and `update` behavior).
* CI now fails early if the generated README “Usage” content is out of
date.

* **Documentation**
* Regenerated the README “Usage” console block to reflect the latest
help text and command details.
* Updated agent guidance to match the expanded README generation
behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-01 16:36:03 +02:00

185 lines
6.5 KiB
Python

# SPDX-FileCopyrightText: © Jens Bergmann and contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
"""CI guard: verifies generated schema, default-config, and README artifacts are up-to-date."""
from __future__ import annotations
import difflib
import subprocess # noqa: S404
import sys
import tempfile
from pathlib import Path
from typing import TYPE_CHECKING, Final
from generate_readme_commands import build_usage_block, replace_marked_section
from schema_utils import generate_schema_content
from kleinanzeigen_bot.model.ad_model import AdPartial
from kleinanzeigen_bot.model.config_model import Config
if TYPE_CHECKING:
from pydantic import BaseModel
SCHEMA_DEFINITIONS:Final[tuple[tuple[str, type[BaseModel], str], ...]] = (
("schemas/config.schema.json", Config, "Config"),
("schemas/ad.schema.json", AdPartial, "Ad"),
)
DEFAULT_CONFIG_PATH:Final[Path] = Path("docs/config.default.yaml")
def generate_default_config_via_cli(path:Path, repo_root:Path) -> None:
"""
Run `python -m kleinanzeigen_bot --config <path> create-config` to generate a default config snapshot.
"""
try:
subprocess.run( # noqa: S603 trusted, static command arguments
[
sys.executable,
"-m",
"kleinanzeigen_bot",
"--config",
str(path),
"create-config",
],
cwd = repo_root,
check = True,
timeout = 60,
capture_output = True,
text = True,
)
except subprocess.CalledProcessError as error:
stderr = error.stderr.strip() if error.stderr else "<empty>"
stdout = error.stdout.strip() if error.stdout else "<empty>"
raise RuntimeError(
"Failed to generate default config via CLI.\n"
f"Return code: {error.returncode}\n"
f"stderr:\n{stderr}\n"
f"stdout:\n{stdout}"
) from error
def get_schema_diffs(repo_root:Path) -> dict[str, str]:
"""
Compare committed schema files with freshly generated schema content and return unified diffs per path.
"""
diffs:dict[str, str] = {}
for schema_path, model, schema_name in SCHEMA_DEFINITIONS:
expected_schema_path = repo_root / schema_path
expected = expected_schema_path.read_text(encoding = "utf-8") if expected_schema_path.is_file() else ""
generated = generate_schema_content(model, schema_name)
if expected == generated:
continue
diffs[schema_path] = "".join(
difflib.unified_diff(
expected.splitlines(keepends = True),
generated.splitlines(keepends = True),
fromfile = schema_path,
tofile = f"<generated via: {model.__name__}.model_json_schema>",
)
)
return diffs
def get_default_config_diff(repo_root:Path) -> str:
"""
Compare docs/config.default.yaml with a freshly generated config artifact and return a unified diff string.
"""
expected_config_path = repo_root / DEFAULT_CONFIG_PATH
if not expected_config_path.is_file():
raise FileNotFoundError(f"Missing required default config file: {DEFAULT_CONFIG_PATH}")
with tempfile.TemporaryDirectory() as tmpdir:
generated_config_path = Path(tmpdir) / "config.default.yaml"
generate_default_config_via_cli(generated_config_path, repo_root)
expected = expected_config_path.read_text(encoding = "utf-8")
generated = generated_config_path.read_text(encoding = "utf-8")
if expected == generated:
return ""
return "".join(
difflib.unified_diff(
expected.splitlines(keepends = True),
generated.splitlines(keepends = True),
fromfile = str(DEFAULT_CONFIG_PATH),
tofile = "<generated via: python -m kleinanzeigen_bot --config /path/to/config.default.yaml create-config>",
)
)
def get_readme_diff(repo_root:Path) -> str:
"""
Compare committed README.md with in-memory generated content and return a unified diff string.
Never mutates README.md — computes expected content entirely in memory.
"""
readme_path = repo_root / "README.md"
if not readme_path.is_file():
raise FileNotFoundError(f"Missing README.md: {readme_path}")
committed = readme_path.read_text(encoding = "utf-8")
new_block = build_usage_block()
try:
expected = replace_marked_section(committed, new_block)
except ValueError as exc:
raise RuntimeError(f"README marker validation failed: {exc}") from exc
if committed == expected:
return ""
return "".join(
difflib.unified_diff(
committed.splitlines(keepends = True),
expected.splitlines(keepends = True),
fromfile = "README.md",
tofile = "<generated via: pdm run generate-readme-commands>",
)
)
def main() -> None:
repo_root = Path(__file__).resolve().parent.parent
try:
schema_diffs = get_schema_diffs(repo_root)
default_config_diff = get_default_config_diff(repo_root)
readme_diff = get_readme_diff(repo_root)
except (FileNotFoundError, RuntimeError) as exc:
raise SystemExit(f"Error while checking generated artifacts: {exc}") from exc
if schema_diffs or default_config_diff or readme_diff:
messages:list[str] = ["Generated artifacts are not up-to-date."]
if schema_diffs:
messages.append("Outdated schema files detected:")
for path, schema_diff in schema_diffs.items():
messages.append(f"- {path}")
messages.append(schema_diff)
if default_config_diff:
messages.append("Outdated docs/config.default.yaml detected.")
messages.append(default_config_diff)
if readme_diff:
messages.append("Outdated README.md detected (Usage section).")
messages.append(readme_diff)
messages.append("Regenerate with one of the following:")
messages.append("- Schema files: pdm run generate-schemas")
messages.append("- Default config snapshot: pdm run generate-config")
messages.append("- README Usage section: pdm run generate-readme-commands")
messages.append("- All: pdm run generate-artifacts")
raise SystemExit("\n".join(messages))
print("Generated schemas, docs/config.default.yaml, and README.md are up-to-date.")
if __name__ == "__main__":
main()