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 <noreply@anthropic.com>
This commit is contained in:
1 parent
2237779e56
commit
7de6984ebc
4 files changed
+42
-12
No files matched your search
@@ -17,11 +17,15 @@ build = importlib.util.module_from_spec(spec)
|
|||||||
spec.loader.exec_module(build)
|
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
|
@pytest.fixture
|
||||||
def release(tmp_path):
|
def release(tmp_path):
|
||||||
directory = tmp_path / "build"
|
directory = tmp_path / "build"
|
||||||
directory.mkdir()
|
directory.mkdir()
|
||||||
for name in deploy.PACKAGE_FILES:
|
for name in deploy.PACKAGE_FILES + TEMPLATE_NAMES:
|
||||||
path = directory / name
|
path = directory / name
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
path.write_bytes(b"test release payload")
|
path.write_bytes(b"test release payload")
|
||||||
@@ -33,13 +37,15 @@ def release(tmp_path):
|
|||||||
|
|
||||||
def test_deployment_packages_release_without_work_files(release):
|
def test_deployment_packages_release_without_work_files(release):
|
||||||
directory, target = release
|
directory, target = release
|
||||||
|
for name in IGNORED_NAMES:
|
||||||
|
(directory / name).write_bytes(b"ignored")
|
||||||
for folder in ["input", "output"]:
|
for folder in ["input", "output"]:
|
||||||
(directory / folder).mkdir()
|
(directory / folder).mkdir()
|
||||||
(directory / folder / "private.xlsx").write_bytes(b"private")
|
(directory / folder / "private.xlsx").write_bytes(b"private")
|
||||||
result = deploy.deploy(directory, target)
|
result = deploy.deploy(directory, target)
|
||||||
assert result.name == "Tätigkeitsnachweis-Generator v1.3.0.zip"
|
assert result.name == "Tätigkeitsnachweis-Generator v1.3.0.zip"
|
||||||
with zipfile.ZipFile(result) as archive:
|
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 archive.testzip() is None
|
||||||
assert not list(target.glob(".upload-*"))
|
assert not list(target.glob(".upload-*"))
|
||||||
# An unchanged release can be retried without overwriting the published ZIP.
|
# An unchanged release can be retried without overwriting the published ZIP.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from src.writer import generate_activity_reports
|
|||||||
|
|
||||||
CONFIG_DIR = PROJECT_ROOT / "config"
|
CONFIG_DIR = PROJECT_ROOT / "config"
|
||||||
INPUT_FILE = Path(__file__).resolve().parent / "fixture" / "2026 JM.xlsx"
|
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")
|
@pytest.fixture(scope="module")
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from datetime import date
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from deploy import deploy, write_release_manifest
|
from deploy import deploy, is_template_pdf, write_release_manifest
|
||||||
|
|
||||||
# ── Pfade ───────────────────────────────────────────────────────
|
# ── Pfade ───────────────────────────────────────────────────────
|
||||||
BUILD_PIPELINE_DIR = Path(__file__).resolve().parent
|
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(f, config_dst / f.name)
|
||||||
|
|
||||||
shutil.copy2(CHANGELOG_PATH, BUILD_OUTPUT_DIR / "CHANGELOG.md")
|
shutil.copy2(CHANGELOG_PATH, BUILD_OUTPUT_DIR / "CHANGELOG.md")
|
||||||
shutil.copy2(
|
|
||||||
APP_DIR / "template" / "Tätigkeitsnachweise Stellenförderung.pdf",
|
# Alle Template-PDFs ausliefern (ohne Dateien mit '_' oder '.' am Anfang).
|
||||||
BUILD_OUTPUT_DIR / "template" / "Tätigkeitsnachweise Stellenförderung.pdf",
|
# 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
|
# README für input-Ordner
|
||||||
input_readme = BUILD_OUTPUT_DIR / "input" / "_DATEIEN_HIER_ABLEGEN_.txt"
|
input_readme = BUILD_OUTPUT_DIR / "input" / "_DATEIEN_HIER_ABLEGEN_.txt"
|
||||||
|
|||||||
@@ -18,10 +18,27 @@ MANIFEST_NAME = "release.json"
|
|||||||
PACKAGE_FILES = [
|
PACKAGE_FILES = [
|
||||||
EXE_NAME, "README.md", "CHANGELOG.md",
|
EXE_NAME, "README.md", "CHANGELOG.md",
|
||||||
"config/settings.ini", "config/input-fields.json", "config/output-fields.json",
|
"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:
|
def normalize_version(version: str) -> str:
|
||||||
if not re.fullmatch(r"\d+\.\d+(?:\.\d+)?", version):
|
if not re.fullmatch(r"\d+\.\d+(?:\.\d+)?", version):
|
||||||
raise ValueError(f"Ungültige Versionsnummer: {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."""
|
"""Record the version and all payload hashes after a successful build."""
|
||||||
manifest = {
|
manifest = {
|
||||||
"version": normalize_version(version),
|
"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(
|
(build_dir / MANIFEST_NAME).write_text(
|
||||||
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
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.")
|
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"))
|
manifest = json.loads((build_dir / MANIFEST_NAME).read_text(encoding="utf-8"))
|
||||||
version = normalize_version(manifest["version"])
|
version = normalize_version(manifest["version"])
|
||||||
for name in PACKAGE_FILES:
|
for name in package_files(build_dir):
|
||||||
path = build_dir / name
|
path = build_dir / name
|
||||||
if not path.is_file() or file_hash(path) != manifest.get("files", {}).get(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.")
|
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)
|
temporary = Path(temporary)
|
||||||
try:
|
try:
|
||||||
with zipfile.ZipFile(temporary, "w", zipfile.ZIP_DEFLATED) as package:
|
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)
|
package.write(build_dir / name, name)
|
||||||
for directory in ["input/", "output/"]:
|
for directory in ["input/", "output/"]:
|
||||||
# Fixed timestamps keep repeated deployments byte-identical.
|
# Fixed timestamps keep repeated deployments byte-identical.
|
||||||
|
|||||||
Reference in new issue
Block a user