mirror of
https://github.com/Second-Hand-Friends/kleinanzeigen-bot.git
synced 2026-03-12 02:31:45 +01:00
## ℹ️ Description - Link to the related issue(s): N/A - Add a CI guard that fails when generated artifacts are out of sync, motivated by preventing missing schema updates and keeping generated reference files current. - Add a committed `docs/config.default.yaml` as a user-facing default configuration reference. ## 📋 Changes Summary - Add `scripts/check_generated_artifacts.py` to regenerate schema artifacts and compare tracked outputs (`schemas/*.json` and `docs/config.default.yaml`) against generated content. - Run the new artifact consistency check in CI via `.github/workflows/build.yml`. - Add `pdm run generate-config` and `pdm run generate-artifacts` tasks, with a cross-platform-safe delete in `generate-config`. - Add generated `docs/config.default.yaml` and document it in `docs/CONFIGURATION.md`. - Update `schemas/config.schema.json` with the `diagnostics.timing_collection` property generated from the model. ### ⚙️ 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 * **Documentation** * Added a reference link to the default configuration snapshot for easier access to baseline settings. * **Chores** * Added a CI build-time check that validates generated schemas and the default config and alerts when regeneration is needed. * Added scripts to generate the default config and to sequence artifact generation. * Added a utility to produce standardized schema content and compare generated artifacts. * Minor tweak to schema generation success messaging. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
144 lines
5.0 KiB
Python
144 lines
5.0 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 and default-config 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 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 main() -> None:
|
|
repo_root = Path(__file__).resolve().parent.parent
|
|
|
|
schema_diffs = get_schema_diffs(repo_root)
|
|
default_config_diff = get_default_config_diff(repo_root)
|
|
|
|
if schema_diffs or default_config_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)
|
|
|
|
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("- Both: pdm run generate-artifacts")
|
|
raise SystemExit("\n".join(messages))
|
|
|
|
print("Generated schemas and docs/config.default.yaml are up-to-date.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|