Validate complete Core release bundle sources
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
@@ -762,6 +763,141 @@ class ReleaseExecutionTests(unittest.TestCase):
|
||||
_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:
|
||||
return RepositorySpec(
|
||||
|
||||
@@ -7,6 +7,7 @@ import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import signal
|
||||
import stat
|
||||
@@ -76,6 +77,9 @@ DURABLE_REMOTE = "origin"
|
||||
FLATPAK_BUILDER_PROXY = Path("/usr/bin/flatpak-spawn")
|
||||
HOST_BUBBLEWRAP = Path("/usr/bin/bwrap")
|
||||
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):
|
||||
@@ -1631,9 +1635,122 @@ def _clone_catalog_sources(
|
||||
tag=f"v{version.removeprefix('v')}",
|
||||
source_root=destination,
|
||||
)
|
||||
_clone_release_bundle_sources(
|
||||
result=result,
|
||||
specs=specs,
|
||||
source_root=destination,
|
||||
)
|
||||
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(
|
||||
*, receipt: dict[str, Any] | None, workspace_root: Path
|
||||
) -> dict[str, Any]:
|
||||
|
||||
Reference in New Issue
Block a user