mirror of
https://github.com/Second-Hand-Friends/kleinanzeigen-bot.git
synced 2026-03-12 02:31:45 +01:00
ci: check generated schema and default config artifacts (#825)
## ℹ️ 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 -->
This commit is contained in:
143
scripts/check_generated_artifacts.py
Normal file
143
scripts/check_generated_artifacts.py
Normal file
@@ -0,0 +1,143 @@
|
||||
# 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()
|
||||
@@ -1,33 +1,28 @@
|
||||
# SPDX-FileCopyrightText: © Sebastian Thomschke and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Type
|
||||
|
||||
from pydantic import BaseModel
|
||||
from schema_utils import generate_schema_content
|
||||
|
||||
from kleinanzeigen_bot.model.ad_model import AdPartial
|
||||
from kleinanzeigen_bot.model.config_model import Config
|
||||
|
||||
|
||||
def generate_schema(model:Type[BaseModel], name:str, out_dir:Path) -> None:
|
||||
def generate_schema(model:type[BaseModel], name:str, out_dir:Path) -> None:
|
||||
"""
|
||||
Generate and write JSON schema for the given model.
|
||||
"""
|
||||
print(f"[+] Generating schema for model [{name}]...")
|
||||
|
||||
# Create JSON Schema dict
|
||||
schema = model.model_json_schema(mode = "validation")
|
||||
schema.setdefault("title", f"{name} Schema")
|
||||
schema.setdefault("description", f"Auto-generated JSON Schema for {name}")
|
||||
schema_content = generate_schema_content(model, name)
|
||||
|
||||
# Write JSON
|
||||
json_path = out_dir / f"{name.lower()}.schema.json"
|
||||
with json_path.open("w", encoding = "utf-8") as f_json:
|
||||
json.dump(schema, f_json, indent = 2)
|
||||
f_json.write("\n")
|
||||
print(f"[✓] {json_path}")
|
||||
with json_path.open("w", encoding = "utf-8") as json_file:
|
||||
json_file.write(schema_content)
|
||||
print(f"[OK] {json_path}")
|
||||
|
||||
|
||||
project_root = Path(__file__).parent.parent
|
||||
|
||||
21
scripts/schema_utils.py
Normal file
21
scripts/schema_utils.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# SPDX-FileCopyrightText: © Jens Bergmann and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
def generate_schema_content(model:type[BaseModel], name:str) -> str:
|
||||
"""
|
||||
Build normalized JSON schema output for project models.
|
||||
"""
|
||||
schema = model.model_json_schema(mode = "validation")
|
||||
schema.setdefault("title", f"{name} Schema")
|
||||
schema.setdefault("description", f"Auto-generated JSON Schema for {name}")
|
||||
return json.dumps(schema, indent = 2) + "\n"
|
||||
Reference in New Issue
Block a user