From 492449a4e2e9a38f4dde12d16e27d6d6d9066d77 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Tue, 4 Aug 2026 15:06:36 +0200 Subject: [PATCH] Align all release version declarations --- docs/PACKAGE_REGISTRY_RELEASES.md | 5 +++ tests/test_release_entrypoint_gates.py | 17 +++++++++ tools/release/push-release-tag.sh | 48 ++++++++++++++++++++------ 3 files changed, 60 insertions(+), 10 deletions(-) diff --git a/docs/PACKAGE_REGISTRY_RELEASES.md b/docs/PACKAGE_REGISTRY_RELEASES.md index aa875ec..e9f9aa3 100644 --- a/docs/PACKAGE_REGISTRY_RELEASES.md +++ b/docs/PACKAGE_REGISTRY_RELEASES.md @@ -66,6 +66,11 @@ tags. The default preflight intentionally does not require those heads to exist in the previous release baseline. A failed candidate-baseline check therefore cannot produce a protected package release. +The source gate validates `pyproject.toml`, module `MODULE_VERSION`, public +package `__version__`, and WebUI metadata before creating tags. Release-tag +artifact checks run only after the candidate tags and immutable WebUI lock have +been created locally. + It builds one wheel and, where applicable, one npm tarball. The workflow records the source tag, source commit, filename, size, and SHA-256 in `package-artifacts.json` before publishing. Gitea rejects a second upload of the diff --git a/tests/test_release_entrypoint_gates.py b/tests/test_release_entrypoint_gates.py index a40a29e..6914d89 100644 --- a/tests/test_release_entrypoint_gates.py +++ b/tests/test_release_entrypoint_gates.py @@ -61,6 +61,23 @@ class ReleaseEntrypointGateTests(unittest.TestCase): self.assertNotIn("--strict-if-baseline", audit_function) self.assertIn('command+=("--strict")', audit_function) + def test_source_gate_does_not_require_tags_before_they_are_created(self) -> None: + script = (META_ROOT / "tools" / "release" / "push-release-tag.sh").read_text() + gate = script[ + script.index("run_version_alignment_gate()") : + script.index("run_manifest_shape_gate()") + ] + + self.assertIn('command+=(--source-metadata-only)', gate) + self.assertIn('else\n command+=(--release-composition)', gate) + + def test_version_updater_targets_canonical_runtime_declarations(self) -> None: + script = (META_ROOT / "tools" / "release" / "push-release-tag.sh").read_text() + + self.assertIn("could not update MODULE_VERSION", script) + self.assertIn("update_package_init_versions", script) + self.assertNotIn("could not update ModuleManifest.version", script) + def test_source_catalog_generator_enforces_explicit_repo_versions(self) -> None: script = (META_ROOT / "tools" / "release" / "generate-release-catalog.py").read_text() diff --git a/tools/release/push-release-tag.sh b/tools/release/push-release-tag.sh index 71e2069..c30fcab 100644 --- a/tools/release/push-release-tag.sh +++ b/tools/release/push-release-tag.sh @@ -333,20 +333,46 @@ path = pathlib.Path(sys.argv[1]) new_version = sys.argv[2] text = path.read_text() text, count = re.subn( - r'(?m)^(\s*version=)["\'][^"\']+["\'](,?\s*)$', + r'(?m)^(MODULE_VERSION\s*=\s*)["\'][^"\']+["\'](\s*)$', rf'\1"{new_version}"\2', text, count=1, ) -if count == 0: - text, count = re.subn( - r'(?m)^(MODULE_VERSION\s*=\s*)["\'][^"\']+["\'](\s*)$', - rf'\1"{new_version}"\2', - text, - count=1, - ) if count != 1: - raise SystemExit(f"could not update ModuleManifest.version in {path}") + raise SystemExit(f"could not update MODULE_VERSION in {path}") +path.write_text(text) +PYCODE + done +} + +update_package_init_versions() { + local repo="$1" + local version="$2" + local package_init="" + + for package_init in "$repo"/src/*/__init__.py; do + [[ -f "$package_init" ]] || continue + if ! grep -q '^__version__\s*=' "$package_init"; then + continue + fi + "$PYTHON" - "$package_init" "$version" <<'PYCODE' +from __future__ import annotations + +import pathlib +import re +import sys + +path = pathlib.Path(sys.argv[1]) +new_version = sys.argv[2] +text = path.read_text() +text, count = re.subn( + r'(?m)^(__version__\s*=\s*)["\'][^"\']+["\'](\s*)$', + rf'\1"{new_version}"\2', + text, + count=1, +) +if count != 1: + raise SystemExit(f"could not update __version__ in {path}") path.write_text(text) PYCODE done @@ -470,6 +496,7 @@ update_version_files() { update_pyproject "$repo" "$version" update_manifest_version "$repo" "$project_name" "$version" + update_package_init_versions "$repo" "$version" update_webui_package "$repo" "$project_name" "$version" } @@ -500,10 +527,11 @@ run_version_alignment_gate() { "$PYTHON" "$META_ROOT/tools/checks/check-version-alignment.py" --workspace-root "$PARENT" - --release-composition ) if [[ "$mode" == "source" ]]; then command+=(--source-metadata-only) + else + command+=(--release-composition) fi local repo for repo in "${PACKAGE_REPOS[@]}"; do