Release v0.1.15
Dependency Audit / dependency-audit (push) Failing after 1m49s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m50s
Developer Meta-package Release / publish-package (push) Failing after 4s

This commit is contained in:
2026-08-04 15:20:50 +02:00
parent 774793976c
commit a24c94435e
8 changed files with 265 additions and 88 deletions
+14
View File
@@ -72,6 +72,20 @@ The source gate validates `pyproject.toml`, the module version declaration
checks run only after the candidate tags and immutable WebUI lock have been checks run only after the candidate tags and immutable WebUI lock have been
created locally. created locally.
Release-lock regeneration resolves a fresh immutable lock from the reviewed
candidate manifests; it does not seed resolution from the previous release
lock. This prevents removed transitive packages and stale peer metadata from
blocking or contaminating the new release. Candidate resolution also uses an
isolated temporary npm cache, so a locally replaced tag cannot reuse metadata
from a failed, unpushed release attempt.
Modules that retain the same WebUI package identity in both a root publish
manifest and `webui/package.json` use the WebUI manifest as the canonical peer
contract. The coordinated release synchronizes `peerDependencies` and
`peerDependenciesMeta` into the publish manifest before creating the module
tag, then synchronizes each lockfile root from the final package metadata. A
distinct root package remains independent.
It builds one wheel and, where applicable, one npm tarball. The workflow records It builds one wheel and, where applicable, one npm tarball. The workflow records
the source tag, source commit, filename, size, and SHA-256 in the source tag, source commit, filename, size, and SHA-256 in
`package-artifacts.json` before publishing. Gitea rejects a second upload of the `package-artifacts.json` before publishing. Gitea rejects a second upload of the
+67 -67
View File
@@ -4,82 +4,82 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "govoplan" name = "govoplan"
version = "0.1.14" version = "0.1.15"
description = "Developer convenience package for a versioned GovOPlaN composition" description = "Developer convenience package for a versioned GovOPlaN composition"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
license = { text = "AGPL-3.0-or-later" } license = { text = "AGPL-3.0-or-later" }
dependencies = [ dependencies = [
"govoplan-core[server]==0.1.14", "govoplan-core[server]==0.1.15",
"govoplan-tenancy==0.1.8", "govoplan-tenancy==0.1.15",
"govoplan-organizations==0.1.8", "govoplan-organizations==0.1.15",
"govoplan-identity==0.1.8", "govoplan-identity==0.1.15",
"govoplan-idm==0.1.8", "govoplan-idm==0.1.15",
"govoplan-access==0.1.8", "govoplan-access==0.1.15",
"govoplan-admin==0.1.8", "govoplan-admin==0.1.15",
"govoplan-policy==0.1.8", "govoplan-policy==0.1.15",
"govoplan-audit==0.1.8", "govoplan-audit==0.1.15",
"govoplan-dashboard==0.1.8", "govoplan-dashboard==0.1.15",
"govoplan-files==0.1.8", "govoplan-files==0.1.15",
"govoplan-mail==0.1.10", "govoplan-mail==0.1.15",
"govoplan-campaign==0.1.11", "govoplan-campaign==0.1.15",
"govoplan-calendar==0.1.8", "govoplan-calendar==0.1.15",
"govoplan-docs==0.1.8", "govoplan-docs==0.1.15",
"govoplan-ops==0.1.8", "govoplan-ops==0.1.15",
] ]
[project.optional-dependencies] [project.optional-dependencies]
full = [ full = [
"govoplan-addresses==0.1.9", "govoplan-addresses==0.1.15",
"govoplan-approvals==0.1.14", "govoplan-approvals==0.1.15",
"govoplan-assets==0.1.8", "govoplan-assets==0.1.15",
"govoplan-booking==0.1.8", "govoplan-booking==0.1.15",
"govoplan-cases==0.1.8", "govoplan-cases==0.1.15",
"govoplan-certificates==0.1.8", "govoplan-certificates==0.1.15",
"govoplan-committee==0.1.8", "govoplan-committee==0.1.15",
"govoplan-connectors==0.1.14", "govoplan-connectors==0.1.15",
"govoplan-consultation==0.1.8", "govoplan-consultation==0.1.15",
"govoplan-contracts==0.1.8", "govoplan-contracts==0.1.15",
"govoplan-dataflow==0.1.14", "govoplan-dataflow==0.1.15",
"govoplan-datasources==0.1.14", "govoplan-datasources==0.1.15",
"govoplan-decisions==0.1.14", "govoplan-decisions==0.1.15",
"govoplan-dist-lists==0.1.14", "govoplan-dist-lists==0.1.15",
"govoplan-encryption==0.1.14", "govoplan-encryption==0.1.15",
"govoplan-evaluation==0.1.8", "govoplan-evaluation==0.1.15",
"govoplan-facilities==0.1.8", "govoplan-facilities==0.1.15",
"govoplan-forms==0.1.14", "govoplan-forms==0.1.15",
"govoplan-forms-runtime==0.1.14", "govoplan-forms-runtime==0.1.15",
"govoplan-grants==0.1.8", "govoplan-grants==0.1.15",
"govoplan-helpdesk==0.1.8", "govoplan-helpdesk==0.1.15",
"govoplan-identity-trust==0.1.14", "govoplan-identity-trust==0.1.15",
"govoplan-inspections==0.1.8", "govoplan-inspections==0.1.15",
"govoplan-learning==0.1.8", "govoplan-learning==0.1.15",
"govoplan-mandates==0.1.14", "govoplan-mandates==0.1.15",
"govoplan-notifications==0.1.8", "govoplan-notifications==0.1.15",
"govoplan-parties==0.1.14", "govoplan-parties==0.1.15",
"govoplan-permits==0.1.8", "govoplan-permits==0.1.15",
"govoplan-poll==0.1.11", "govoplan-poll==0.1.15",
"govoplan-portal==0.1.8", "govoplan-portal==0.1.15",
"govoplan-postbox==0.1.2", "govoplan-postbox==0.1.15",
"govoplan-procurement==0.1.8", "govoplan-procurement==0.1.15",
"govoplan-projects==0.1.14", "govoplan-projects==0.1.15",
"govoplan-records==0.1.8", "govoplan-records==0.1.15",
"govoplan-reporting==0.1.14", "govoplan-reporting==0.1.15",
"govoplan-resources==0.1.8", "govoplan-resources==0.1.15",
"govoplan-rest==0.1.7", "govoplan-rest==0.1.15",
"govoplan-risk-compliance==0.1.8", "govoplan-risk-compliance==0.1.15",
"govoplan-scheduling==0.1.11", "govoplan-scheduling==0.1.15",
"govoplan-search==0.1.14", "govoplan-search==0.1.15",
"govoplan-services==0.1.14", "govoplan-services==0.1.15",
"govoplan-soap==0.1.7", "govoplan-soap==0.1.15",
"govoplan-templates==0.1.14", "govoplan-templates==0.1.15",
"govoplan-tickets==0.1.8", "govoplan-tickets==0.1.15",
"govoplan-transparency==0.1.8", "govoplan-transparency==0.1.15",
"govoplan-views==0.1.0", "govoplan-views==0.1.15",
"govoplan-voting==0.1.14", "govoplan-voting==0.1.15",
"govoplan-wiki==0.1.14", "govoplan-wiki==0.1.15",
"govoplan-workflow==0.1.14", "govoplan-workflow==0.1.15",
"govoplan-workflow-engine==0.1.14", "govoplan-workflow-engine==0.1.15",
] ]
[project.urls] [project.urls]
+15 -15
View File
@@ -1,18 +1,18 @@
# Whole-product release install from immutable, independently versioned module tags. # Whole-product release install from immutable, independently versioned module tags.
# Only add a module after its referenced tag has been published. # Only add a module after its referenced tag has been published.
../govoplan-core[server] ../govoplan-core[server]
govoplan-tenancy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tenancy.git@v0.1.8 govoplan-tenancy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tenancy.git@v0.1.15
govoplan-organizations @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git@v0.1.8 govoplan-organizations @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git@v0.1.15
govoplan-identity @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-identity.git@v0.1.8 govoplan-identity @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-identity.git@v0.1.15
govoplan-idm @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git@v0.1.8 govoplan-idm @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git@v0.1.15
govoplan-access @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git@v0.1.8 govoplan-access @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git@v0.1.15
govoplan-admin @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git@v0.1.8 govoplan-admin @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git@v0.1.15
govoplan-policy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git@v0.1.8 govoplan-policy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git@v0.1.15
govoplan-audit @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git@v0.1.8 govoplan-audit @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git@v0.1.15
govoplan-dashboard @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git@v0.1.8 govoplan-dashboard @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git@v0.1.15
govoplan-files @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git@v0.1.8 govoplan-files @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git@v0.1.15
govoplan-mail @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git@v0.1.10 govoplan-mail @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git@v0.1.15
govoplan-campaign @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git@v0.1.11 govoplan-campaign @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git@v0.1.15
govoplan-calendar @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git@v0.1.8 govoplan-calendar @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git@v0.1.15
govoplan-docs @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git@v0.1.8 govoplan-docs @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git@v0.1.15
govoplan-ops @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git@v0.1.8 govoplan-ops @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git@v0.1.15
+16
View File
@@ -77,8 +77,24 @@ class ReleaseEntrypointGateTests(unittest.TestCase):
self.assertIn("^manifest\\s*=\\s*ModuleManifest", script) self.assertIn("^manifest\\s*=\\s*ModuleManifest", script)
self.assertIn("could not update module version declaration", script) self.assertIn("could not update module version declaration", script)
self.assertIn("update_package_init_versions", script) self.assertIn("update_package_init_versions", script)
self.assertIn("synchronize-webui-package-metadata.py", script)
self.assertIn('"peerDependenciesMeta",', script)
self.assertLess(
script.index('synchronize-webui-package-metadata.py" --repo "$repo"'),
script.index('synchronize_lockfile_root "$package_path"', script.index('synchronize-webui-package-metadata.py" --repo "$repo"')),
)
self.assertNotIn("could not update ModuleManifest.version", script) self.assertNotIn("could not update ModuleManifest.version", script)
def test_release_lock_refreshes_candidate_govoplan_metadata(self) -> None:
script = (META_ROOT / "tools" / "release" / "generate-release-lock.sh").read_text()
self.assertEqual(2, script.count('"npm_config_cache=$TMP_DIR/npm-cache"'))
self.assertNotIn(
'cp "$WEBUI/package-lock.release.json" "$TMP_DIR/package-lock.json"',
script,
)
self.assertIn('cp "$WEBUI/package.release.json" "$TMP_DIR/package.json"', script)
def test_source_catalog_generator_enforces_explicit_repo_versions(self) -> None: def test_source_catalog_generator_enforces_explicit_repo_versions(self) -> None:
script = (META_ROOT / "tools" / "release" / "generate-release-catalog.py").read_text() script = (META_ROOT / "tools" / "release" / "generate-release-catalog.py").read_text()
+71
View File
@@ -0,0 +1,71 @@
from __future__ import annotations
import json
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
META_ROOT = Path(__file__).resolve().parents[1]
SCRIPT = META_ROOT / "tools" / "release" / "synchronize-webui-package-metadata.py"
class SynchronizeWebuiPackageMetadataTests(unittest.TestCase):
def test_copies_peer_contract_without_changing_publish_paths(self) -> None:
with tempfile.TemporaryDirectory() as directory:
repo = Path(directory)
(repo / "webui").mkdir()
(repo / "package.json").write_text(
json.dumps(
{
"name": "@govoplan/example-webui",
"exports": {".": "./webui/src/index.ts"},
"peerDependencies": {"vite": "^6"},
}
)
)
(repo / "webui" / "package.json").write_text(
json.dumps(
{
"name": "@govoplan/example-webui",
"peerDependencies": {"vite": "^7"},
"peerDependenciesMeta": {"vite": {"optional": True}},
}
)
)
subprocess.run(
[sys.executable, str(SCRIPT), "--repo", str(repo)],
check=True,
capture_output=True,
text=True,
)
package = json.loads((repo / "package.json").read_text())
self.assertEqual({"vite": "^7"}, package["peerDependencies"])
self.assertEqual({"vite": {"optional": True}}, package["peerDependenciesMeta"])
self.assertEqual({".": "./webui/src/index.ts"}, package["exports"])
def test_leaves_distinct_root_and_webui_packages_separate(self) -> None:
with tempfile.TemporaryDirectory() as directory:
repo = Path(directory)
(repo / "webui").mkdir()
(repo / "package.json").write_text(json.dumps({"name": "@govoplan/one"}))
(repo / "webui" / "package.json").write_text(json.dumps({"name": "@govoplan/two"}))
subprocess.run(
[sys.executable, str(SCRIPT), "--repo", str(repo)],
check=True,
capture_output=True,
text=True,
)
root = json.loads((repo / "package.json").read_text())
self.assertEqual("@govoplan/one", root["name"])
self.assertNotIn("peerDependencies", root)
if __name__ == "__main__":
unittest.main()
+8 -5
View File
@@ -94,9 +94,6 @@ cleanup() {
trap cleanup EXIT trap cleanup EXIT
cp "$WEBUI/package.release.json" "$TMP_DIR/package.json" cp "$WEBUI/package.release.json" "$TMP_DIR/package.json"
if [[ -f "$WEBUI/package-lock.release.json" ]]; then
cp "$WEBUI/package-lock.release.json" "$TMP_DIR/package-lock.json"
fi
echo "Generating release lockfile from $WEBUI/package.release.json" echo "Generating release lockfile from $WEBUI/package.release.json"
echo "Temporary workspace: $TMP_DIR" echo "Temporary workspace: $TMP_DIR"
@@ -120,7 +117,10 @@ GIT_ENV+=("GIT_CONFIG_COUNT=$git_config_count")
( (
cd "$TMP_DIR" cd "$TMP_DIR"
"${GIT_ENV[@]}" PATH="$(dirname "$NPM_BIN"):$PATH" "$NPM_BIN" install --package-lock-only --ignore-scripts "${GIT_ENV[@]}" \
"npm_config_cache=$TMP_DIR/npm-cache" \
PATH="$(dirname "$NPM_BIN"):$PATH" \
"$NPM_BIN" install --package-lock-only --ignore-scripts
mapfile -t GIT_PACKAGES < <( mapfile -t GIT_PACKAGES < <(
PATH="$(dirname "$NODE_BIN"):$PATH" "$NODE_BIN" <<'NODE' PATH="$(dirname "$NODE_BIN"):$PATH" "$NODE_BIN" <<'NODE'
const fs = require("fs"); const fs = require("fs");
@@ -136,7 +136,10 @@ NODE
) )
if [[ "${#GIT_PACKAGES[@]}" -gt 0 ]]; then if [[ "${#GIT_PACKAGES[@]}" -gt 0 ]]; then
echo "Refreshing git package lock entries: ${GIT_PACKAGES[*]}" echo "Refreshing git package lock entries: ${GIT_PACKAGES[*]}"
"${GIT_ENV[@]}" PATH="$(dirname "$NPM_BIN"):$PATH" "$NPM_BIN" update --package-lock-only --ignore-scripts "${GIT_PACKAGES[@]}" "${GIT_ENV[@]}" \
"npm_config_cache=$TMP_DIR/npm-cache" \
PATH="$(dirname "$NPM_BIN"):$PATH" \
"$NPM_BIN" update --package-lock-only --ignore-scripts "${GIT_PACKAGES[@]}"
fi fi
) )
+18 -1
View File
@@ -413,6 +413,11 @@ if project_name != "govoplan-core":
peers["@govoplan/core-webui"] = f"^{new_version}" peers["@govoplan/core-webui"] = f"^{new_version}"
path.write_text(json.dumps(data, indent=2) + "\n") path.write_text(json.dumps(data, indent=2) + "\n")
PYCODE PYCODE
done
"$PYTHON" "$META_ROOT/tools/release/synchronize-webui-package-metadata.py" --repo "$repo"
for package_path in "$repo/package.json" "$repo/webui/package.json"; do
[[ -f "$package_path" ]] || continue
synchronize_lockfile_root "$package_path" "${package_path%package.json}package-lock.json" synchronize_lockfile_root "$package_path" "${package_path%package.json}package-lock.json"
done done
@@ -461,7 +466,19 @@ if not isinstance(version, str) or not version:
lock["version"] = version lock["version"] = version
packages = lock.get("packages") packages = lock.get("packages")
if isinstance(packages, dict) and isinstance(packages.get(""), dict): if isinstance(packages, dict) and isinstance(packages.get(""), dict):
packages[""]["version"] = version root = packages[""]
root["version"] = version
for group in (
"dependencies",
"devDependencies",
"optionalDependencies",
"peerDependencies",
"peerDependenciesMeta",
):
if group in package:
root[group] = package[group]
else:
root.pop(group, None)
lock_path.write_text(json.dumps(lock, indent=2) + "\n") lock_path.write_text(json.dumps(lock, indent=2) + "\n")
PYCODE PYCODE
} }
@@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""Synchronize duplicated publish and development WebUI package contracts."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
SYNCHRONIZED_KEYS = ("peerDependencies", "peerDependenciesMeta")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo", type=Path, required=True)
args = parser.parse_args()
root_path = args.repo / "package.json"
webui_path = args.repo / "webui" / "package.json"
if not root_path.exists() or not webui_path.exists():
return 0
root = _load(root_path)
webui = _load(webui_path)
root_name = root.get("name")
webui_name = webui.get("name")
if not isinstance(root_name, str) or root_name != webui_name:
return 0
changed = False
for key in SYNCHRONIZED_KEYS:
if key in webui:
value = webui[key]
if root.get(key) != value:
root[key] = value
changed = True
elif key in root:
del root[key]
changed = True
if changed:
root_path.write_text(json.dumps(root, indent=2) + "\n")
print(f"Synchronized WebUI peer metadata in {root_path}")
return 0
def _load(path: Path) -> dict[str, object]:
payload = json.loads(path.read_text())
if not isinstance(payload, dict):
raise SystemExit(f"package metadata must be an object: {path}")
return payload
if __name__ == "__main__":
raise SystemExit(main())