From 7de6984ebca0e6d9fd4f5ff0f9dedaf6067963d3 Mon Sep 17 00:00:00 2001 From: Rob Date: Tue, 3 Nov 2026 11:53:55 +0100 Subject: [PATCH] build: alle Template-PDFs automatisch in Build und Release aufnehmen Alle *.pdf im template-Ordner werden ausgeliefert; Dateien mit '_' oder '.' am Anfang sowie andere Dateitypen werden ignoriert. Veraltete PDFs im Build-Ordner werden entfernt. Tests entsprechend angepasst. Co-Authored-By: Claude Sonnet 5.5 --- 0_application/tests/test_build_pipeline.py | 10 +++++++-- 0_application/tests/test_e2e.py | 2 +- 1_build-pipeline/build.py | 17 ++++++++++----- 1_build-pipeline/deploy.py | 25 ++++++++++++++++++---- 4 files changed, 42 insertions(+), 12 deletions(-) diff --git a/0_application/tests/test_build_pipeline.py b/0_application/tests/test_build_pipeline.py index 6c415b4..d824b4b 100644 --- a/0_application/tests/test_build_pipeline.py +++ b/0_application/tests/test_build_pipeline.py @@ -17,11 +17,15 @@ build = importlib.util.module_from_spec(spec) spec.loader.exec_module(build) +TEMPLATE_NAMES = ["template/A Vorlage.pdf", "template/B Vorlage.PDF"] +IGNORED_NAMES = ["template/_alt.pdf", "template/.versteckt.pdf", "template/A Vorlage.odg"] + + @pytest.fixture def release(tmp_path): directory = tmp_path / "build" directory.mkdir() - for name in deploy.PACKAGE_FILES: + for name in deploy.PACKAGE_FILES + TEMPLATE_NAMES: path = directory / name path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(b"test release payload") @@ -33,13 +37,15 @@ def release(tmp_path): def test_deployment_packages_release_without_work_files(release): directory, target = release + for name in IGNORED_NAMES: + (directory / name).write_bytes(b"ignored") for folder in ["input", "output"]: (directory / folder).mkdir() (directory / folder / "private.xlsx").write_bytes(b"private") result = deploy.deploy(directory, target) assert result.name == "Tätigkeitsnachweis-Generator v1.3.0.zip" with zipfile.ZipFile(result) as archive: - assert set(archive.namelist()) == set(deploy.PACKAGE_FILES + ["input/", "output/"]) + assert set(archive.namelist()) == set(deploy.PACKAGE_FILES + TEMPLATE_NAMES + ["input/", "output/"]) assert archive.testzip() is None assert not list(target.glob(".upload-*")) # An unchanged release can be retried without overwriting the published ZIP. diff --git a/0_application/tests/test_e2e.py b/0_application/tests/test_e2e.py index 1ac9b2e..cda2711 100644 --- a/0_application/tests/test_e2e.py +++ b/0_application/tests/test_e2e.py @@ -15,7 +15,7 @@ from src.writer import generate_activity_reports CONFIG_DIR = PROJECT_ROOT / "config" INPUT_FILE = Path(__file__).resolve().parent / "fixture" / "2026 JM.xlsx" -TEMPLATE_FILE = PROJECT_ROOT / "template" / "Tätigkeitsnachweise Stellenförderung.pdf" +TEMPLATE_FILE = PROJECT_ROOT / "template" / "06-2026 Tätigkeitsnachweise Stellenförderung.pdf" @pytest.fixture(scope="module") diff --git a/1_build-pipeline/build.py b/1_build-pipeline/build.py index e0f6d3b..265e5c6 100644 --- a/1_build-pipeline/build.py +++ b/1_build-pipeline/build.py @@ -21,7 +21,7 @@ from datetime import date from pathlib import Path from typing import Optional -from deploy import deploy, write_release_manifest +from deploy import deploy, is_template_pdf, write_release_manifest # ── Pfade ─────────────────────────────────────────────────────── BUILD_PIPELINE_DIR = Path(__file__).resolve().parent @@ -223,10 +223,17 @@ def _prepare_build_dir() -> None: shutil.copy2(f, config_dst / f.name) shutil.copy2(CHANGELOG_PATH, BUILD_OUTPUT_DIR / "CHANGELOG.md") - shutil.copy2( - APP_DIR / "template" / "Tätigkeitsnachweise Stellenförderung.pdf", - BUILD_OUTPUT_DIR / "template" / "Tätigkeitsnachweise Stellenförderung.pdf", - ) + + # Alle Template-PDFs ausliefern (ohne Dateien mit '_' oder '.' am Anfang). + # Veraltete PDFs aus früheren Builds werden entfernt. + tpl_src = APP_DIR / "template" + tpl_dst = BUILD_OUTPUT_DIR / "template" + wanted = {p.name for p in tpl_src.iterdir() if is_template_pdf(p)} + for old_pdf in tpl_dst.iterdir(): + if is_template_pdf(old_pdf) and old_pdf.name not in wanted: + old_pdf.unlink() + for name in sorted(wanted): + shutil.copy2(tpl_src / name, tpl_dst / name) # README für input-Ordner input_readme = BUILD_OUTPUT_DIR / "input" / "_DATEIEN_HIER_ABLEGEN_.txt" diff --git a/1_build-pipeline/deploy.py b/1_build-pipeline/deploy.py index 561145b..5d163bb 100644 --- a/1_build-pipeline/deploy.py +++ b/1_build-pipeline/deploy.py @@ -18,10 +18,27 @@ MANIFEST_NAME = "release.json" PACKAGE_FILES = [ EXE_NAME, "README.md", "CHANGELOG.md", "config/settings.ini", "config/input-fields.json", "config/output-fields.json", - "template/Tätigkeitsnachweise Stellenförderung.pdf", ] +def is_template_pdf(path: Path) -> bool: + """Nur *.pdf, die nicht mit '_' oder '.' beginnen, gelten als Template.""" + return path.is_file() and path.suffix.lower() == ".pdf" and not path.name.startswith(("_", ".")) + + +def template_files(base_dir: Path) -> list[str]: + """Relative Pfade aller Template-PDFs in base_dir/template (sortiert).""" + folder = base_dir / "template" + if not folder.is_dir(): + return [] + return [f"template/{p.name}" for p in sorted(folder.iterdir()) if is_template_pdf(p)] + + +def package_files(build_dir: Path) -> list[str]: + """Alle Dateien des Release-Pakets: feste Dateien plus alle Template-PDFs.""" + return PACKAGE_FILES + template_files(build_dir) + + def normalize_version(version: str) -> str: if not re.fullmatch(r"\d+\.\d+(?:\.\d+)?", version): raise ValueError(f"Ungültige Versionsnummer: {version}") @@ -40,7 +57,7 @@ def write_release_manifest(build_dir: Path, version: str) -> None: """Record the version and all payload hashes after a successful build.""" manifest = { "version": normalize_version(version), - "files": {name: file_hash(build_dir / name) for name in PACKAGE_FILES}, + "files": {name: file_hash(build_dir / name) for name in package_files(build_dir)}, } (build_dir / MANIFEST_NAME).write_text( json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") @@ -51,7 +68,7 @@ def validate_release(build_dir: Path) -> str: raise ValueError("Kein bestätigter Build vorhanden. Bitte zuerst build.bat ausführen.") manifest = json.loads((build_dir / MANIFEST_NAME).read_text(encoding="utf-8")) version = normalize_version(manifest["version"]) - for name in PACKAGE_FILES: + for name in package_files(build_dir): path = build_dir / name if not path.is_file() or file_hash(path) != manifest.get("files", {}).get(name): raise ValueError(f"Build-Datei fehlt oder wurde nach dem Build verändert: {name}. Bitte neu bauen.") @@ -83,7 +100,7 @@ def deploy(build_dir: Path = BUILD_DIR, target_dir: Path = DEPLOY_DIR) -> Path: temporary = Path(temporary) try: with zipfile.ZipFile(temporary, "w", zipfile.ZIP_DEFLATED) as package: - for name in PACKAGE_FILES: + for name in package_files(build_dir): package.write(build_dir / name, name) for directory in ["input/", "output/"]: # Fixed timestamps keep repeated deployments byte-identical.