Validate complete Core release bundle sources
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -762,6 +763,141 @@ class ReleaseExecutionTests(unittest.TestCase):
|
|||||||
_git_output(cloned["govoplan-core"], "rev-parse", "--abbrev-ref", "HEAD"),
|
_git_output(cloned["govoplan-core"], "rev-parse", "--abbrev-ref", "HEAD"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_catalog_synthesis_adds_exact_core_bundle_dependency(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
workspace = Path(temp_dir)
|
||||||
|
idm_repo, idm_remote = _tagged_repository(
|
||||||
|
workspace,
|
||||||
|
repo="govoplan-idm",
|
||||||
|
bare="idm.git",
|
||||||
|
version="0.1.8",
|
||||||
|
)
|
||||||
|
idm_commit = _git_output(idm_repo, "rev-parse", "v0.1.8^{commit}")
|
||||||
|
(idm_repo / "pyproject.toml").write_text(
|
||||||
|
'[project]\nname = "govoplan-idm"\nversion = "0.1.9"\n',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
_git(idm_repo, "add", "pyproject.toml")
|
||||||
|
_git(idm_repo, "commit", "-m", "Next release")
|
||||||
|
_git(idm_repo, "tag", "-a", "v0.1.9", "-m", "Next release")
|
||||||
|
_git(idm_repo, "push", "origin", "main", "refs/tags/v0.1.9")
|
||||||
|
catalog_commit = _git_output(idm_repo, "rev-parse", "v0.1.9^{commit}")
|
||||||
|
core_repo = workspace / "govoplan-core"
|
||||||
|
core_remote = workspace / "core.git"
|
||||||
|
_git(workspace, "init", "--bare", str(core_remote))
|
||||||
|
_git(workspace, "init", "-b", "main", str(core_repo))
|
||||||
|
_git(core_repo, "config", "user.name", "Release Test")
|
||||||
|
_git(core_repo, "config", "user.email", "release@example.test")
|
||||||
|
(core_repo / "webui").mkdir()
|
||||||
|
(core_repo / "pyproject.toml").write_text(
|
||||||
|
'[project]\nname = "govoplan-core"\nversion = "1.0.0"\n',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
reference = (
|
||||||
|
"git+ssh://git@example.test/add-ideas/"
|
||||||
|
"govoplan-idm.git#v0.1.8"
|
||||||
|
)
|
||||||
|
(core_repo / "webui/package.release.json").write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {"@govoplan/idm-webui": reference},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(core_repo / "webui/package-lock.release.json").write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@govoplan/idm-webui": reference,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"node_modules/@govoplan/idm-webui": {
|
||||||
|
"version": "0.1.8",
|
||||||
|
"resolved": f"{reference.rsplit('#', 1)[0]}#{idm_commit}",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
_git(core_repo, "add", ".")
|
||||||
|
_git(core_repo, "commit", "-m", "Release")
|
||||||
|
_git(core_repo, "tag", "-a", "v1.0.0", "-m", "Release")
|
||||||
|
_git(core_repo, "remote", "add", "origin", str(core_remote))
|
||||||
|
_git(core_repo, "push", "-u", "origin", "main", "refs/tags/v1.0.0")
|
||||||
|
specs = (
|
||||||
|
RepositorySpec(
|
||||||
|
name="govoplan-core",
|
||||||
|
category="system",
|
||||||
|
subtype="kernel",
|
||||||
|
remote=str(core_remote),
|
||||||
|
path="govoplan-core",
|
||||||
|
),
|
||||||
|
RepositorySpec(
|
||||||
|
name="govoplan-idm",
|
||||||
|
category="module",
|
||||||
|
subtype="platform",
|
||||||
|
remote=str(idm_remote),
|
||||||
|
path="govoplan-idm",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"govoplan_release.release_execution.load_repository_specs",
|
||||||
|
return_value=specs,
|
||||||
|
):
|
||||||
|
synthesis = workspace / "synthesis"
|
||||||
|
synthesis.mkdir()
|
||||||
|
cloned = _clone_catalog_sources(
|
||||||
|
source_versions={"govoplan-core": "1.0.0"},
|
||||||
|
repo_versions={},
|
||||||
|
source_receipts={},
|
||||||
|
workspace_root=workspace,
|
||||||
|
destination=synthesis,
|
||||||
|
)
|
||||||
|
newer_synthesis = workspace / "newer-synthesis"
|
||||||
|
newer_synthesis.mkdir()
|
||||||
|
cloned_with_newer_catalog_source = _clone_catalog_sources(
|
||||||
|
source_versions={
|
||||||
|
"govoplan-core": "1.0.0",
|
||||||
|
"govoplan-idm": "0.1.9",
|
||||||
|
},
|
||||||
|
repo_versions={},
|
||||||
|
source_receipts={},
|
||||||
|
workspace_root=workspace,
|
||||||
|
destination=newer_synthesis,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
{"govoplan-core", "govoplan-idm"},
|
||||||
|
set(cloned),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
idm_commit,
|
||||||
|
_git_output(cloned["govoplan-idm"], "rev-parse", "HEAD"),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
catalog_commit,
|
||||||
|
_git_output(
|
||||||
|
cloned_with_newer_catalog_source["govoplan-idm"],
|
||||||
|
"rev-parse",
|
||||||
|
"HEAD",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
idm_commit,
|
||||||
|
_git_output(
|
||||||
|
cloned_with_newer_catalog_source["govoplan-idm"],
|
||||||
|
"rev-parse",
|
||||||
|
"v0.1.8^{commit}",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _repository_spec(remote: Path) -> RepositorySpec:
|
def _repository_spec(remote: Path) -> RepositorySpec:
|
||||||
return RepositorySpec(
|
return RepositorySpec(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import hashlib
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import signal
|
import signal
|
||||||
import stat
|
import stat
|
||||||
@@ -76,6 +77,9 @@ DURABLE_REMOTE = "origin"
|
|||||||
FLATPAK_BUILDER_PROXY = Path("/usr/bin/flatpak-spawn")
|
FLATPAK_BUILDER_PROXY = Path("/usr/bin/flatpak-spawn")
|
||||||
HOST_BUBBLEWRAP = Path("/usr/bin/bwrap")
|
HOST_BUBBLEWRAP = Path("/usr/bin/bwrap")
|
||||||
FLATPAK_HOST_BUS = Path("/run/flatpak/bus")
|
FLATPAK_HOST_BUS = Path("/run/flatpak/bus")
|
||||||
|
_WEBUI_RELEASE_SOURCE = re.compile(
|
||||||
|
r"/(?P<repo>govoplan-[a-z0-9-]+)\.git#v(?P<version>[0-9]+\.[0-9]+\.[0-9]+)$"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ReleaseExecutionError(RuntimeError):
|
class ReleaseExecutionError(RuntimeError):
|
||||||
@@ -1631,9 +1635,122 @@ def _clone_catalog_sources(
|
|||||||
tag=f"v{version.removeprefix('v')}",
|
tag=f"v{version.removeprefix('v')}",
|
||||||
source_root=destination,
|
source_root=destination,
|
||||||
)
|
)
|
||||||
|
_clone_release_bundle_sources(
|
||||||
|
result=result,
|
||||||
|
specs=specs,
|
||||||
|
source_root=destination,
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _clone_release_bundle_sources(
|
||||||
|
*,
|
||||||
|
result: dict[str, Path],
|
||||||
|
specs: dict[str, Any],
|
||||||
|
source_root: Path,
|
||||||
|
) -> None:
|
||||||
|
"""Add exact Core bundle dependencies needed only for alignment checks."""
|
||||||
|
|
||||||
|
core = result.get("govoplan-core")
|
||||||
|
if core is None:
|
||||||
|
raise ReleaseExecutionBlocked(
|
||||||
|
"Authenticated catalog composition has no Core source."
|
||||||
|
)
|
||||||
|
package_path = core / "webui" / "package.release.json"
|
||||||
|
lock_path = core / "webui" / "package-lock.release.json"
|
||||||
|
if not package_path.exists() and not lock_path.exists():
|
||||||
|
return
|
||||||
|
package = _read_bounded_release_json(
|
||||||
|
package_path,
|
||||||
|
label="Core release WebUI package",
|
||||||
|
)
|
||||||
|
lock = _read_bounded_release_json(
|
||||||
|
lock_path,
|
||||||
|
label="Core release WebUI lock",
|
||||||
|
)
|
||||||
|
dependencies = package.get("dependencies")
|
||||||
|
lock_packages = lock.get("packages")
|
||||||
|
if not isinstance(dependencies, dict) or not isinstance(lock_packages, dict):
|
||||||
|
raise ReleaseExecutionBlocked(
|
||||||
|
"Core release WebUI dependency metadata is incomplete."
|
||||||
|
)
|
||||||
|
if len(dependencies) > MAX_CATALOG_SOURCE_REPOSITORIES:
|
||||||
|
raise ReleaseExecutionBlocked(
|
||||||
|
"Core release WebUI dependency set exceeds its bound."
|
||||||
|
)
|
||||||
|
for package_name, reference in sorted(dependencies.items()):
|
||||||
|
if not isinstance(package_name, str) or not package_name.startswith(
|
||||||
|
"@govoplan/"
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
match = (
|
||||||
|
_WEBUI_RELEASE_SOURCE.search(reference)
|
||||||
|
if isinstance(reference, str)
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
if match is None:
|
||||||
|
raise ReleaseExecutionBlocked(
|
||||||
|
f"Core release dependency {package_name} has no immutable source tag."
|
||||||
|
)
|
||||||
|
repo = match.group("repo")
|
||||||
|
version = match.group("version")
|
||||||
|
repository = specs.get(repo)
|
||||||
|
if repository is None:
|
||||||
|
raise ReleaseExecutionBlocked(
|
||||||
|
f"Core release dependency {repo} is not registered."
|
||||||
|
)
|
||||||
|
locked = lock_packages.get(f"node_modules/{package_name}")
|
||||||
|
resolved = locked.get("resolved") if isinstance(locked, dict) else None
|
||||||
|
locked_version = locked.get("version") if isinstance(locked, dict) else None
|
||||||
|
commit = (
|
||||||
|
resolved.rsplit("#", 1)[-1]
|
||||||
|
if isinstance(resolved, str) and "#" in resolved
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
locked_version != version
|
||||||
|
or not re.fullmatch(r"[0-9a-f]{40}|[0-9a-f]{64}", commit)
|
||||||
|
):
|
||||||
|
raise ReleaseExecutionBlocked(
|
||||||
|
f"Core release dependency {repo} has no exact lock identity."
|
||||||
|
)
|
||||||
|
if repo not in result:
|
||||||
|
if len(result) >= MAX_CATALOG_SOURCE_REPOSITORIES:
|
||||||
|
raise ReleaseExecutionBlocked(
|
||||||
|
"Release composition source set exceeds its bound."
|
||||||
|
)
|
||||||
|
result[repo] = _checkout_registered_source_tag(
|
||||||
|
repo=repo,
|
||||||
|
source_remote=repository.remote,
|
||||||
|
tag=f"v{version}",
|
||||||
|
source_root=source_root,
|
||||||
|
)
|
||||||
|
observed_type = git_text(
|
||||||
|
result[repo], "cat-file", "-t", f"refs/tags/v{version}"
|
||||||
|
)
|
||||||
|
observed = ref_commit(result[repo], f"refs/tags/v{version}")
|
||||||
|
if observed_type != "tag" or observed != commit:
|
||||||
|
raise ReleaseExecutionBlocked(
|
||||||
|
f"Core release dependency {repo} lock does not match its origin tag."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _read_bounded_release_json(path: Path, *, label: str) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
encoded = path.read_bytes()
|
||||||
|
except OSError as exc:
|
||||||
|
raise ReleaseExecutionBlocked(f"{label} cannot be read.") from exc
|
||||||
|
if len(encoded) > 4 * 1024 * 1024:
|
||||||
|
raise ReleaseExecutionBlocked(f"{label} exceeds its size limit.")
|
||||||
|
try:
|
||||||
|
payload = json.loads(encoded)
|
||||||
|
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||||
|
raise ReleaseExecutionBlocked(f"{label} is malformed.") from exc
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
raise ReleaseExecutionBlocked(f"{label} must be a JSON object.")
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
def verify_frozen_repository_tag_receipt(
|
def verify_frozen_repository_tag_receipt(
|
||||||
*, receipt: dict[str, Any] | None, workspace_root: Path
|
*, receipt: dict[str, Any] | None, workspace_root: Path
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
|
|||||||
Reference in New Issue
Block a user