Add registry-backed module package releases
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
META_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class ModulePackageWorkflowTests(unittest.TestCase):
|
||||
def test_template_enforces_tag_version_hash_and_registry_contract(self) -> None:
|
||||
workflow = (
|
||||
META_ROOT / "tools/repo/templates/module-package-release.yml"
|
||||
).read_text(encoding="utf-8")
|
||||
|
||||
self.assertIn("tag_protections", workflow)
|
||||
self.assertIn("git merge-base --is-ancestor", workflow)
|
||||
self.assertIn("does not match", workflow)
|
||||
self.assertIn("package-artifacts.json", workflow)
|
||||
self.assertIn("api/packages/GovOPlaN/pypi", workflow)
|
||||
self.assertIn("api/packages/GovOPlaN/npm", workflow)
|
||||
self.assertIn("GOVOPLAN_PACKAGE_TOKEN", workflow)
|
||||
self.assertIn("must resolve to an exact registry version", workflow)
|
||||
self.assertIn("git\\\\.add-ideas\\\\.de/GovOPlaN", workflow)
|
||||
self.assertIn("release package identity does not match", workflow)
|
||||
self.assertNotIn("Generic", workflow)
|
||||
|
||||
@unittest.skipUnless(shutil.which("node"), "Node.js is required")
|
||||
def test_webui_publication_normalizes_internal_git_dependencies(self) -> None:
|
||||
workflow = (
|
||||
META_ROOT / "tools/repo/templates/module-package-release.yml"
|
||||
).read_text(encoding="utf-8")
|
||||
marker = " node <<'NODE'\n"
|
||||
script = workflow.split(marker, 1)[1].split("\n NODE", 1)[0]
|
||||
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
package_dir = root / ".package-webui"
|
||||
package_dir.mkdir()
|
||||
package_path = package_dir / "package.json"
|
||||
package_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"name": "@govoplan/core-webui",
|
||||
"version": "0.1.14",
|
||||
"private": True,
|
||||
"dependencies": {
|
||||
"@govoplan/access-webui": (
|
||||
"git+ssh://git@git.add-ideas.de/GovOPlaN/"
|
||||
"govoplan-access.git#v0.1.11"
|
||||
)
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
subprocess.run(
|
||||
["node"],
|
||||
input=script,
|
||||
cwd=root,
|
||||
check=True,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
package = json.loads(package_path.read_text(encoding="utf-8"))
|
||||
self.assertNotIn("private", package)
|
||||
self.assertEqual(
|
||||
"0.1.11", package["dependencies"]["@govoplan/access-webui"]
|
||||
)
|
||||
|
||||
def test_sync_script_only_targets_packageable_govoplan_repositories(self) -> None:
|
||||
namespace: dict[str, object] = {
|
||||
"__file__": str(META_ROOT / "tools/repo/sync-module-package-workflows.py"),
|
||||
"__name__": "test_sync_module_package_workflows",
|
||||
}
|
||||
script = (META_ROOT / "tools/repo/sync-module-package-workflows.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
exec(compile(script, str(namespace["__file__"]), "exec"), namespace)
|
||||
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
parent = Path(temporary)
|
||||
package_repositories = namespace["package_repositories"]
|
||||
# The production inventory is authoritative, so a temporary parent
|
||||
# only exposes matching paths that are present in that inventory.
|
||||
known = parent / "govoplan-core"
|
||||
known.mkdir()
|
||||
(known / "pyproject.toml").write_text("[project]\n", encoding="utf-8")
|
||||
self.assertEqual(package_repositories(parent), (known,))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,167 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from io import BytesIO
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import tomllib
|
||||
import unittest
|
||||
import zipfile
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _load(name: str, path: Path):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
PACKAGE_SET = _load(
|
||||
"generate_release_package_set",
|
||||
ROOT / "tools/release/generate-release-package-set.py",
|
||||
)
|
||||
ARTIFACTS = _load(
|
||||
"resolve_package_artifacts",
|
||||
ROOT / "tools/release/resolve-package-artifacts.py",
|
||||
)
|
||||
|
||||
|
||||
class PackageRegistryReleaseTests(unittest.TestCase):
|
||||
def test_current_release_sources_form_a_hash_bound_package_set(self) -> None:
|
||||
core_version = tomllib.loads(
|
||||
(ROOT.parent / "govoplan-core/pyproject.toml").read_text(encoding="utf-8")
|
||||
)["project"]["version"]
|
||||
payload = PACKAGE_SET.generate_package_set(
|
||||
core_version=core_version,
|
||||
requirements=ROOT / "requirements-release.txt",
|
||||
workspace=ROOT.parent,
|
||||
)
|
||||
|
||||
self.assertEqual("1", payload["schema_version"])
|
||||
self.assertEqual("govoplan-core", payload["python"][0]["name"])
|
||||
self.assertIn(
|
||||
"@govoplan/core-webui",
|
||||
{item["name"] for item in payload["webui"]},
|
||||
)
|
||||
unsigned = dict(payload)
|
||||
digest = unsigned.pop("package_set_sha256")
|
||||
self.assertEqual(ARTIFACTS._canonical_sha256(unsigned), digest)
|
||||
|
||||
def test_wheel_and_webui_artifacts_are_verified_by_embedded_identity(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-package-artifacts-") as value:
|
||||
root = Path(value)
|
||||
wheels = root / "wheels"
|
||||
webui = root / "webui"
|
||||
wheels.mkdir()
|
||||
webui.mkdir()
|
||||
wheel = wheels / "govoplan_demo-1.2.3-py3-none-any.whl"
|
||||
with zipfile.ZipFile(wheel, "w") as archive:
|
||||
archive.writestr(
|
||||
"govoplan_demo-1.2.3.dist-info/METADATA",
|
||||
"Metadata-Version: 2.1\nName: govoplan-demo\nVersion: 1.2.3\n",
|
||||
)
|
||||
package_json = json.dumps(
|
||||
{"name": "@govoplan/demo-webui", "version": "1.2.3"}
|
||||
).encode("utf-8")
|
||||
npm = webui / "govoplan-demo-webui-1.2.3.tgz"
|
||||
with tarfile.open(npm, "w:gz") as archive:
|
||||
member = tarfile.TarInfo("package/package.json")
|
||||
member.size = len(package_json)
|
||||
archive.addfile(member, BytesIO(package_json))
|
||||
source = {
|
||||
"version": "1.2.3",
|
||||
"repository": "govoplan-demo",
|
||||
"tag": "v1.2.3",
|
||||
"commit": "1" * 40,
|
||||
}
|
||||
|
||||
python_rows = ARTIFACTS._verify_wheels(
|
||||
({"name": "govoplan-demo", "extras": ["server"], **source},), wheels
|
||||
)
|
||||
webui_rows = ARTIFACTS._verify_webui(
|
||||
({"name": "@govoplan/demo-webui", **source},), webui
|
||||
)
|
||||
|
||||
self.assertEqual("govoplan-demo", python_rows[0]["name"])
|
||||
self.assertEqual(["server"], python_rows[0]["extras"])
|
||||
self.assertEqual("@govoplan/demo-webui", webui_rows[0]["name"])
|
||||
self.assertTrue(str(webui_rows[0]["integrity"]).startswith("sha512-"))
|
||||
|
||||
def test_package_set_rejects_argument_shaped_package_names(self) -> None:
|
||||
payload = {
|
||||
"schema_version": "1",
|
||||
"release_version": "1.2.3",
|
||||
"registries": {
|
||||
"python": "https://packages.example.test/pypi/simple",
|
||||
"npm": "https://packages.example.test/npm/",
|
||||
},
|
||||
"python": [
|
||||
{
|
||||
"name": "--index-url",
|
||||
"version": "1.2.3",
|
||||
"repository": "govoplan-demo",
|
||||
"extras": [],
|
||||
"tag": "v1.2.3",
|
||||
"commit": "1" * 40,
|
||||
}
|
||||
],
|
||||
"webui": [
|
||||
{
|
||||
"name": "@govoplan/demo-webui",
|
||||
"version": "1.2.3",
|
||||
"repository": "govoplan-demo",
|
||||
"tag": "v1.2.3",
|
||||
"commit": "1" * 40,
|
||||
}
|
||||
],
|
||||
}
|
||||
payload["package_set_sha256"] = ARTIFACTS._canonical_sha256(payload)
|
||||
with tempfile.TemporaryDirectory() as value:
|
||||
path = Path(value) / "packages.json"
|
||||
path.write_text(json.dumps(payload), encoding="utf-8")
|
||||
with self.assertRaisesRegex(
|
||||
ARTIFACTS.PackageArtifactError, "invalid identity"
|
||||
):
|
||||
ARTIFACTS._load_package_set(path)
|
||||
|
||||
def test_runtime_workflow_consumes_registry_artifacts_and_publishes_lock(self) -> None:
|
||||
workflow = (ROOT / ".gitea/workflows/runtime-distribution.yml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
self.assertIn("resolve-package-artifacts.py", workflow)
|
||||
self.assertIn("package-artifacts.lock.json", workflow)
|
||||
self.assertIn(
|
||||
"--package-lock runtime-output/package-artifacts.lock.json",
|
||||
workflow,
|
||||
)
|
||||
self.assertNotIn(
|
||||
"pip wheel --no-deps --wheel-dir runtime-output/local-wheels",
|
||||
workflow,
|
||||
)
|
||||
|
||||
def test_developer_meta_package_matches_workspace_versions(self) -> None:
|
||||
script = _load(
|
||||
"generate_developer_meta_package",
|
||||
ROOT / "tools/release/generate-developer-meta-package.py",
|
||||
)
|
||||
expected = script.render(
|
||||
workspace=ROOT.parent,
|
||||
requirements=ROOT / "requirements-release.txt",
|
||||
)
|
||||
actual = (ROOT / "packages/govoplan-meta/pyproject.toml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
@@ -298,12 +299,37 @@ class RuntimeDistributionBuildTests(unittest.TestCase):
|
||||
(root / "web.json").write_text(json.dumps(web_metadata))
|
||||
deployer = root / "govoplan-deploy.pyz"
|
||||
deployer.write_bytes(b"zipapp")
|
||||
package_lock = root / "package-artifacts.lock.json"
|
||||
package_lock_value = {
|
||||
"schema_version": "1",
|
||||
"release_version": "1.2.3",
|
||||
"python": [
|
||||
{
|
||||
"name": "govoplan-core",
|
||||
"version": "1.2.3",
|
||||
"sha256": "8" * 64,
|
||||
}
|
||||
],
|
||||
"webui": [],
|
||||
}
|
||||
package_lock_value["lock_sha256"] = hashlib.sha256(
|
||||
json.dumps(
|
||||
package_lock_value,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
).encode("utf-8")
|
||||
).hexdigest()
|
||||
package_lock.write_text(
|
||||
json.dumps(package_lock_value) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
args = argparse.Namespace(
|
||||
composition=root / "composition.json",
|
||||
api_metadata=root / "api.json",
|
||||
web_metadata=root / "web.json",
|
||||
deployer=deployer,
|
||||
deployer_url="https://downloads.example/govoplan-deploy.pyz",
|
||||
package_lock=package_lock,
|
||||
artifact_base_url="https://downloads.example/runtime/v1.2.3",
|
||||
source_commit="f" * 40,
|
||||
version="1.2.3",
|
||||
@@ -327,6 +353,10 @@ class RuntimeDistributionBuildTests(unittest.TestCase):
|
||||
)
|
||||
self.assertTrue((root / "evidence/api-sbom.cdx.json").is_file())
|
||||
self.assertTrue((root / "evidence/web-provenance.json").is_file())
|
||||
self.assertEqual(
|
||||
hashlib.sha256(package_lock.read_bytes()).hexdigest(),
|
||||
descriptor["package_lock"]["sha256"],
|
||||
)
|
||||
|
||||
def test_rejects_incomplete_oci_index(self) -> None:
|
||||
with self.assertRaisesRegex(ValueError, "linux/amd64 and linux/arm64"):
|
||||
|
||||
Reference in New Issue
Block a user