467 lines
24 KiB
Python
467 lines
24 KiB
Python
from __future__ import annotations
|
|
|
|
import base64
|
|
from contextlib import ExitStack
|
|
import csv
|
|
from copy import deepcopy
|
|
import hashlib
|
|
import io
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import sys
|
|
import tarfile
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import patch
|
|
from urllib.parse import quote
|
|
import zipfile
|
|
|
|
from cryptography.hazmat.primitives import serialization
|
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT / "tools/release"))
|
|
|
|
from govoplan_release import full_catalog # noqa: E402
|
|
from govoplan_release.artifact_identity import selected_artifact_identity_issues # noqa: E402
|
|
from govoplan_release.catalog import canonical_hash # noqa: E402
|
|
from govoplan_release.model import RepositorySpec # noqa: E402
|
|
from govoplan_release.registry_reference import registry_entry_source # noqa: E402
|
|
from govoplan_release.selective_catalog import ( # noqa: E402
|
|
load_authenticated_catalog_base, public_key_base64, signature,
|
|
)
|
|
from govoplan_release.source_provenance import ( # noqa: E402
|
|
SourceTagProvenanceIssue, catalog_source_selection,
|
|
registered_source_origin_issues,
|
|
)
|
|
from govoplan_release.version_alignment import candidate_catalog_version_issues # noqa: E402
|
|
|
|
|
|
class FullRegistryCatalogTests(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.temp = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.temp.cleanup)
|
|
self.root = Path(self.temp.name)
|
|
self.web = self.root / "addideas-govoplan-website"
|
|
self.wheels = self.root / "wheels"
|
|
self.npm = self.root / "npm"
|
|
self.wheels.mkdir(mode=0o700)
|
|
self.npm.mkdir(mode=0o700)
|
|
self.key = Ed25519PrivateKey.generate()
|
|
self.keypath = self.root / "key.pem"
|
|
self.keypath.write_bytes(self.key.private_bytes(
|
|
serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8,
|
|
serialization.NoEncryption(),
|
|
))
|
|
self.keypath.chmod(0o600)
|
|
self.keyring = {
|
|
"keyring_version": "1", "keys": [{
|
|
"key_id": "known-key", "public_key": public_key_base64(self.key),
|
|
"status": "active",
|
|
}],
|
|
}
|
|
self.base = {
|
|
"catalog_version": "1", "channel": "stable", "sequence": 1,
|
|
"core_release": {"version": "1.0.0"}, "modules": [],
|
|
"release": {},
|
|
}
|
|
self.write_base()
|
|
self.package_set = {
|
|
"schema_version": "1", "release_version": "1.2.3", "profile": "full",
|
|
"registries": {
|
|
"python": "https://git.add-ideas.de/api/packages/GovOPlaN/pypi/simple",
|
|
"npm": "https://git.add-ideas.de/api/packages/GovOPlaN/npm/",
|
|
},
|
|
"python": [self.package("govoplan-core"), self.package("govoplan-demo")],
|
|
"webui": [self.package("govoplan-core", webui=True)],
|
|
}
|
|
self.seal(self.package_set, "package_set_sha256")
|
|
self.lock = {
|
|
"schema_version": "1", "release_version": "1.2.3", "profile": "full",
|
|
"registries": self.package_set["registries"],
|
|
"package_set_sha256": self.package_set["package_set_sha256"],
|
|
"python": [], "webui": [],
|
|
}
|
|
for row in self.package_set["python"]:
|
|
path = self.wheel(row["name"])
|
|
url = full_catalog._tool("resolve-package-artifacts")["_python_artifact_url"](
|
|
self.package_set["registries"]["python"], package=row, filename=path.name,
|
|
)
|
|
self.lock["python"].append(self.artifact(row, path, url))
|
|
npm_package = self.package_set["webui"][0]
|
|
npm_path = self.npm / "govoplan-core-webui-1.2.3.tgz"
|
|
self.tarball(npm_path, "@govoplan/core-webui", "1.2.3")
|
|
url = self.package_set["registries"]["npm"] + quote(npm_package["name"], safe="") + "/-/1.2.3/core-webui-1.2.3.tgz"
|
|
row = self.artifact(npm_package, npm_path, url)
|
|
row["integrity"] = "sha512-" + base64.b64encode(hashlib.sha512(npm_path.read_bytes()).digest()).decode()
|
|
self.lock["webui"].append(row)
|
|
self.seal(self.lock, "lock_sha256")
|
|
self.set_path = self.root / "package-set.json"
|
|
self.lock_path = self.root / "package-lock.json"
|
|
self.write_inputs()
|
|
self.output = self.root / "candidate"
|
|
|
|
@staticmethod
|
|
def package(repo: str, *, webui: bool = False) -> dict:
|
|
row = {
|
|
"name": "@govoplan/core-webui" if webui else repo, "version": "1.2.3",
|
|
"repository": repo, "tag": "v1.2.3",
|
|
"commit": ("a" if repo == "govoplan-core" else "b") * 40,
|
|
}
|
|
if not webui:
|
|
row["extras"] = ["server"] if repo == "govoplan-core" else []
|
|
return row
|
|
|
|
@staticmethod
|
|
def artifact(package: dict, path: Path, url: str) -> dict:
|
|
encoded = path.read_bytes()
|
|
return {**package, "filename": path.name, "url": url,
|
|
"sha256": hashlib.sha256(encoded).hexdigest(), "size": len(encoded)}
|
|
|
|
@staticmethod
|
|
def seal(payload: dict, field: str) -> None:
|
|
payload.pop(field, None)
|
|
payload[field] = hashlib.sha256(json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
|
|
|
|
def write_inputs(self) -> None:
|
|
self.set_path.write_text(json.dumps(self.package_set))
|
|
self.lock_path.write_text(json.dumps(self.lock))
|
|
|
|
def write_base(self) -> None:
|
|
self.base.pop("signatures", None)
|
|
self.base["signatures"] = [signature(self.base, key_id="known-key", private_key=self.key)]
|
|
folder = self.web / "public/catalogs/v1"
|
|
(folder / "channels").mkdir(parents=True, exist_ok=True)
|
|
(folder / "channels/stable.json").write_text(json.dumps(self.base))
|
|
(folder / "keyring.json").write_text(json.dumps(self.keyring))
|
|
|
|
def wheel(self, package: str) -> Path:
|
|
stem = package.replace("-", "_")
|
|
info = f"{stem}-1.2.3.dist-info"
|
|
files = {
|
|
f"{stem}/__init__.py": b"VALUE = 1\n",
|
|
f"{info}/METADATA": f"Metadata-Version: 2.1\nName: {package}\nVersion: 1.2.3\n".encode(),
|
|
f"{info}/WHEEL": b"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
|
|
}
|
|
record = io.StringIO()
|
|
writer = csv.writer(record, lineterminator="\n")
|
|
for name, value in files.items():
|
|
writer.writerow((name, "", len(value)))
|
|
writer.writerow((f"{info}/RECORD", "", ""))
|
|
files[f"{info}/RECORD"] = record.getvalue().encode()
|
|
path = self.wheels / f"{stem}-1.2.3-py3-none-any.whl"
|
|
with zipfile.ZipFile(path, "w") as archive:
|
|
for name, value in files.items():
|
|
archive.writestr(name, value)
|
|
return path
|
|
|
|
@staticmethod
|
|
def tarball(path: Path, name: str, version: str, *, duplicate: bool = False) -> None:
|
|
encoded = json.dumps({"name": name, "version": version}).encode()
|
|
with tarfile.open(path, "w:gz") as archive:
|
|
for _ in range(2 if duplicate else 1):
|
|
member = tarfile.TarInfo("package/package.json")
|
|
member.size = len(encoded)
|
|
archive.addfile(member, io.BytesIO(encoded))
|
|
|
|
def build(self, *, provenance_errors=(), origin_errors=()) -> dict:
|
|
registry_generator = full_catalog._tool("generate-release-catalog")
|
|
tools = {name: dict(full_catalog._tool(name)) for name in (
|
|
"generate-release-catalog", "generate-release-package-set", "resolve-package-artifacts",
|
|
)}
|
|
tools["generate-release-package-set"]["generate_package_set"] = lambda **kwargs: self.package_set
|
|
self.provenance = {
|
|
row["repository"]: {"commit_sha": row["commit"], "tag_object_sha": str(index + 1) * 40}
|
|
for index, row in enumerate(self.package_set["python"])
|
|
}
|
|
entry = {
|
|
"module_id": "demo", "name": "Demo", "version": "1.2.3",
|
|
"python_package": "govoplan-demo",
|
|
"python_ref": "govoplan-demo @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-demo.git@v1.2.3",
|
|
}
|
|
with ExitStack() as stack:
|
|
stack.enter_context(patch.object(full_catalog, "_tool", side_effect=lambda name: tools[name]))
|
|
stack.enter_context(patch.dict(registry_generator["_catalog_payload"].__globals__, {
|
|
"synthesize_repository_catalog_entries": lambda **kwargs: (dict(entry),),
|
|
}))
|
|
stack.enter_context(patch.object(full_catalog, "enforce_selected_version_alignment"))
|
|
stack.enter_context(patch.object(full_catalog, "registered_source_origin_issues", return_value=origin_errors))
|
|
self.provenance_check = stack.enter_context(patch.object(full_catalog, "source_tag_provenance_issues", return_value=provenance_errors))
|
|
stack.enter_context(patch.object(full_catalog, "selected_source_provenance", return_value=self.provenance))
|
|
return full_catalog.build_full_registry_candidate(
|
|
package_set_path=self.set_path, package_lock_path=self.lock_path,
|
|
wheelhouse=self.wheels, webui_packages=self.npm, output_dir=self.output,
|
|
selected_repositories=("govoplan-core",),
|
|
signing_keys=(f"known-key={self.keypath}",), workspace_root=self.root,
|
|
)
|
|
|
|
def candidate(self) -> dict:
|
|
return json.loads((self.output / "channels/stable.json").read_text())
|
|
|
|
def test_full_candidate_uses_registry_bytes_and_preserves_unchanged_tag_provenance(self) -> None:
|
|
result = self.build()
|
|
candidate = self.candidate()
|
|
self.assertEqual("ready", result["status"])
|
|
self.assertEqual(2, result["package_count"])
|
|
self.assertEqual(1, result["selected_count"])
|
|
self.assertEqual(self.keyring, json.loads((self.output / "keyring.json").read_text()))
|
|
self.assertEqual(canonical_hash(self.keyring), candidate["release"]["keyring_sha256"])
|
|
self.assertEqual(2, len(candidate["release"]["artifacts"]))
|
|
self.assertIn("/pypi/files/", candidate["core_release"]["python_ref"])
|
|
self.assertEqual((), candidate_catalog_version_issues(candidate))
|
|
self.assertEqual((), selected_artifact_identity_issues(candidate))
|
|
sources = catalog_source_selection(candidate)
|
|
self.assertEqual((), sources.issues)
|
|
self.assertEqual({"govoplan-core": "1.2.3"}, sources.selected_versions)
|
|
self.assertEqual({"govoplan-core": "1.2.3", "govoplan-demo": "1.2.3"}, sources.all_versions)
|
|
self.assertEqual("b" * 40, sources.selected_commits["govoplan-demo"])
|
|
self.assertEqual("2" * 40, sources.selected_tag_objects["govoplan-demo"])
|
|
self.assertEqual(2, self.provenance_check.call_count)
|
|
for call in self.provenance_check.call_args_list:
|
|
self.assertEqual({"govoplan-core"}, call.kwargs["require_head_repos"])
|
|
for path in [self.output, *self.output.rglob("*")]:
|
|
self.assertEqual(0o700 if path.is_dir() else 0o600, path.stat().st_mode & 0o777)
|
|
|
|
def test_legacy_base_is_authenticated_but_remains_rejected_by_selective(self) -> None:
|
|
self.build()
|
|
with self.assertRaisesRegex(ValueError, "does not pin"):
|
|
load_authenticated_catalog_base(
|
|
base_catalog=None, base_keyring=None, web_root=self.web,
|
|
channel="stable", public_base_url="https://unused.example",
|
|
signer_public_keys={"known-key": public_key_base64(self.key)},
|
|
)
|
|
|
|
def test_injected_key_or_mismatched_pinned_keyring_is_rejected(self) -> None:
|
|
for mutation in ("extra-key", "bad-hash"):
|
|
with self.subTest(mutation=mutation):
|
|
if mutation == "extra-key":
|
|
self.keyring["keys"].append({"key_id": "injected", "status": "active", "public_key": public_key_base64(Ed25519PrivateKey.generate())})
|
|
else:
|
|
self.keyring["keys"] = self.keyring["keys"][:1]
|
|
self.base["release"]["keyring_sha256"] = "f" * 64
|
|
self.write_base()
|
|
with self.assertRaises(ValueError):
|
|
self.build()
|
|
self.assertFalse(self.output.exists())
|
|
|
|
def test_tampered_base_signature_is_rejected(self) -> None:
|
|
path = self.web / "public/catalogs/v1/channels/stable.json"
|
|
payload = json.loads(path.read_text())
|
|
payload["sequence"] = 999
|
|
path.write_text(json.dumps(payload))
|
|
with self.assertRaisesRegex(ValueError, "signature verification"):
|
|
self.build()
|
|
|
|
def test_wrong_registry_bytes_and_reused_candidate_fail_closed(self) -> None:
|
|
self.build()
|
|
original = (self.output / "channels/stable.json").read_bytes()
|
|
with self.assertRaisesRegex(ValueError, "must not already exist"):
|
|
self.build()
|
|
self.assertEqual(original, (self.output / "channels/stable.json").read_bytes())
|
|
self.output = self.root / "candidate-2"
|
|
wheel = self.wheels / self.lock["python"][0]["filename"]
|
|
with wheel.open("ab") as stream:
|
|
stream.write(b"tampered")
|
|
with self.assertRaisesRegex(ValueError, "bytes differ"):
|
|
self.build()
|
|
self.assertFalse(self.output.exists())
|
|
|
|
def test_webui_identity_and_url_changes_are_rejected(self) -> None:
|
|
row = self.lock["webui"][0]
|
|
original = row["url"]
|
|
for url in (
|
|
original + "?alternate=true", original.replace("/-/1.2.3/", "/-/9.9.9/"),
|
|
original.replace("/npm/", "/npm/../other/"),
|
|
original.replace("/npm/", "/npm/%2e%2e/other/"),
|
|
original.replace("/npm/", "/npm/%252e%252e/other/"),
|
|
):
|
|
with self.subTest(url=url):
|
|
row["url"] = url
|
|
self.seal(self.lock, "lock_sha256")
|
|
self.write_inputs()
|
|
with self.assertRaisesRegex(ValueError, "URL differs"):
|
|
self.build()
|
|
|
|
def test_duplicate_missing_and_symlinked_artifacts_are_rejected(self) -> None:
|
|
row = self.lock["webui"][0]
|
|
self.lock["webui"].append(dict(row))
|
|
with self.assertRaisesRegex(ValueError, "duplicate/missing"):
|
|
full_catalog.verify_registry_artifacts(package_set=self.package_set, lock=self.lock, wheelhouse=self.wheels, webui_packages=self.npm)
|
|
self.lock["webui"].pop()
|
|
path = self.npm / row["filename"]
|
|
moved = self.root / "moved.tgz"
|
|
path.rename(moved)
|
|
path.symlink_to(moved)
|
|
with self.assertRaises(OSError):
|
|
full_catalog.verify_registry_artifacts(package_set=self.package_set, lock=self.lock, wheelhouse=self.wheels, webui_packages=self.npm)
|
|
|
|
def test_archive_metadata_is_bounded_and_not_ambiguous(self) -> None:
|
|
path = self.npm / "duplicate.tgz"
|
|
self.tarball(path, "@govoplan/core-webui", "1.2.3", duplicate=True)
|
|
with self.assertRaisesRegex(ValueError, "duplicate"):
|
|
full_catalog._inspect_npm_metadata(path, name="@govoplan/core-webui", version="1.2.3")
|
|
|
|
def test_origin_or_tag_provenance_failure_prevents_output(self) -> None:
|
|
issue = SourceTagProvenanceIssue("govoplan-core", "v1.2.3", "wrong immutable identity")
|
|
for kwargs in ({"origin_errors": (issue,)}, {"provenance_errors": (issue,)}):
|
|
with self.subTest(kwargs=kwargs), self.assertRaisesRegex(ValueError, "gate failed"):
|
|
self.build(**kwargs)
|
|
self.assertFalse(self.output.exists())
|
|
|
|
def test_registered_source_origin_requires_exact_fetch_and_push_targets(self) -> None:
|
|
repo = self.root / "govoplan-core"
|
|
repo.mkdir()
|
|
spec = RepositorySpec("govoplan-core", "system", "kernel", "git@example.test:trusted/core.git", "govoplan-core")
|
|
with patch("govoplan_release.source_provenance.load_repository_specs", return_value=(spec,)):
|
|
for targets in ((spec.remote, spec.remote), ("git@evil.test:core.git", spec.remote), (spec.remote, "git@evil.test:core.git")):
|
|
with self.subTest(targets=targets), patch("govoplan_release.source_provenance.git_text", side_effect=targets):
|
|
issues = registered_source_origin_issues(repo_versions={"govoplan-core": "1.2.3"}, workspace=self.root, remote="origin")
|
|
self.assertEqual(targets != (spec.remote, spec.remote), bool(issues))
|
|
|
|
def test_registry_metadata_cannot_cross_wire_webui_or_archive_identity(self) -> None:
|
|
self.build()
|
|
candidate = self.candidate()
|
|
entry = candidate["core_release"]
|
|
entry["webui_package"] = "@govoplan/files-webui"
|
|
with self.assertRaisesRegex(ValueError, "another source repository"):
|
|
registry_entry_source(entry)
|
|
candidate = self.candidate()
|
|
candidate["release"]["artifacts"][0]["archive_sha256"] = "f" * 64
|
|
self.assertIn("matching inspected wheel", " ".join(selected_artifact_identity_issues(candidate)))
|
|
|
|
def test_registry_version_and_selected_source_identity_must_agree(self) -> None:
|
|
self.build()
|
|
for field in ("commit_sha", "tag_object_sha"):
|
|
candidate = self.candidate()
|
|
candidate["release"]["selected_units"][0][field] = "f" * 40
|
|
self.assertIn("differs", " ".join(issue.message for issue in catalog_source_selection(candidate).issues))
|
|
candidate = self.candidate()
|
|
candidate["modules"][0]["artifact_integrity"]["python"]["git_ref"] = "v9.9.9"
|
|
self.assertTrue(candidate_catalog_version_issues(candidate))
|
|
|
|
def test_repeated_module_projections_must_bind_identical_python_and_webui_bytes(self) -> None:
|
|
self.build()
|
|
for kind in ("python", "webui"):
|
|
for reversed_order in (False, True):
|
|
with self.subTest(kind=kind, reversed_order=reversed_order):
|
|
candidate = self.candidate()
|
|
entry = deepcopy(candidate["core_release"])
|
|
entry["module_id"] = "another-core-projection"
|
|
artifact = entry["artifact_integrity"][kind]
|
|
artifact["sha256"] = "f" * 64
|
|
if kind == "python":
|
|
entry["python_ref"] = artifact["ref"] = entry["python_ref"].split("#sha256=", 1)[0] + "#sha256=" + "f" * 64
|
|
if reversed_order:
|
|
original = candidate["core_release"]
|
|
candidate["core_release"] = entry
|
|
entry = original
|
|
candidate["modules"].append(entry)
|
|
self.assertIn(f"conflicting {kind}", " ".join(selected_artifact_identity_issues(candidate)))
|
|
self.assertTrue(candidate_catalog_version_issues(candidate))
|
|
candidate = self.candidate()
|
|
repeated = deepcopy(candidate["modules"][0])
|
|
repeated["module_id"] = "second-demo-projection"
|
|
candidate["modules"].append(repeated)
|
|
self.assertEqual((), selected_artifact_identity_issues(candidate))
|
|
self.assertEqual((), candidate_catalog_version_issues(candidate))
|
|
|
|
def test_metadata_inspection_uses_the_opened_archive_not_a_replaced_path(self) -> None:
|
|
path = self.npm / "original.tgz"
|
|
replacement = self.npm / "replacement.tgz"
|
|
saved = self.npm / "saved.tgz"
|
|
self.tarball(path, "@govoplan/incorrect-webui", "1.2.3")
|
|
self.tarball(replacement, "@govoplan/core-webui", "1.2.3")
|
|
real_open = tarfile.open
|
|
|
|
def replace_path(*args, **kwargs):
|
|
self.assertIn("fileobj", kwargs)
|
|
path.rename(saved)
|
|
replacement.rename(path)
|
|
return real_open(*args, **kwargs)
|
|
|
|
with patch("govoplan_release.full_catalog.tarfile.open", side_effect=replace_path):
|
|
with self.assertRaisesRegex(ValueError, "metadata differs"):
|
|
full_catalog._inspect_npm_metadata(path, name="@govoplan/core-webui", version="1.2.3")
|
|
|
|
def test_registry_url_provenance_rejects_package_version_and_traversal_mismatch(self) -> None:
|
|
self.build()
|
|
for old, new in (
|
|
("/govoplan-core/1.2.3/", "/govoplan-files/1.2.3/"),
|
|
("/govoplan-core/1.2.3/", "/govoplan-core/9.9.9/"),
|
|
("/pypi/files/", "/pypi/files/%2e%2e/"),
|
|
("/pypi/files/", "/pypi/files/%252e%252e/"),
|
|
):
|
|
with self.subTest(new=new):
|
|
entry = self.candidate()["core_release"]
|
|
artifact = entry["artifact_integrity"]["python"]
|
|
artifact["url"] = artifact["url"].replace(old, new)
|
|
artifact["ref"] = entry["python_ref"] = entry["python_ref"].replace(old, new)
|
|
with self.assertRaises(ValueError):
|
|
registry_entry_source(entry)
|
|
|
|
def test_package_set_must_match_fixed_meta_pins_not_just_its_own_hash(self) -> None:
|
|
payload = deepcopy(self.package_set)
|
|
payload["python"][1]["version"] = "9.9.9"
|
|
self.seal(payload, "package_set_sha256")
|
|
self.set_path.write_text(json.dumps(payload))
|
|
with self.assertRaisesRegex(ValueError, "exact Meta full pins"):
|
|
self.build()
|
|
self.assertFalse(self.output.exists())
|
|
|
|
def test_package_set_git_reads_ignore_caller_redirection(self) -> None:
|
|
tool = full_catalog._tool("generate-release-package-set")
|
|
with patch.dict(os.environ, {"GIT_DIR": "/outside", "GIT_CONFIG_GLOBAL": "/outside/config", "PATH": "/outside/bin"}):
|
|
with patch("subprocess.check_output", return_value="a" * 40 + "\n") as execute:
|
|
self.assertEqual("a" * 40, tool["_git"](self.root, "rev-parse", "HEAD"))
|
|
self.assertEqual("/usr/bin/git", execute.call_args.args[0][0])
|
|
self.assertNotIn("GIT_DIR", execute.call_args.kwargs["env"])
|
|
self.assertEqual(os.devnull, execute.call_args.kwargs["env"]["GIT_CONFIG_GLOBAL"])
|
|
self.assertEqual("/usr/bin:/bin", execute.call_args.kwargs["env"]["PATH"])
|
|
|
|
def test_unchanged_real_annotated_ancestor_is_valid_but_selecting_it_requires_head(self) -> None:
|
|
from tests.test_release_source_provenance import git, git_text, make_repo
|
|
from govoplan_release.source_provenance import source_tag_provenance_issues
|
|
|
|
workspace = self.root / "source-workspace"
|
|
workspace.mkdir()
|
|
repo, _remote = make_repo(workspace, self.root, "govoplan-access", "1.2.3")
|
|
git(repo, "tag", "-a", "v1.2.3", "-m", "reviewed immutable package")
|
|
git(repo, "push", "origin", "refs/tags/v1.2.3")
|
|
tagged_commit = git_text(repo, "rev-parse", "HEAD")
|
|
(repo / "workflow-only.txt").write_text("post-tag workflow repair\n")
|
|
git(repo, "add", "workflow-only.txt")
|
|
git(repo, "commit", "-m", "repair workflow without replacing package")
|
|
git(repo, "push", "origin", "main")
|
|
common = {
|
|
"repo_versions": {"govoplan-access": "1.2.3"}, "workspace": workspace,
|
|
"expected_commits": {"govoplan-access": tagged_commit},
|
|
"expected_tag_objects": {"govoplan-access": git_text(repo, "rev-parse", "refs/tags/v1.2.3")},
|
|
}
|
|
self.assertEqual((), source_tag_provenance_issues(**common))
|
|
issues = source_tag_provenance_issues(**common, require_head_repos=("govoplan-access",))
|
|
self.assertIn("not selected HEAD", " ".join(issue.message for issue in issues))
|
|
|
|
def test_tagged_manifest_synthesis_ignores_local_git_replacement_objects(self) -> None:
|
|
from tests.test_release_source_provenance import git, git_text, make_repo
|
|
from govoplan_release.catalog_entry_synthesis import materialized_source_tree
|
|
|
|
workspace = self.root / "materialization-workspace"
|
|
workspace.mkdir()
|
|
repo, _remote = make_repo(workspace, self.root, "govoplan-access", "1.2.3")
|
|
original = (repo / "pyproject.toml").read_text()
|
|
tagged_commit = git_text(repo, "rev-parse", "HEAD")
|
|
git(repo, "tag", "-a", "v1.2.3", "-m", "reviewed immutable package")
|
|
(repo / "pyproject.toml").write_text(original.replace("1.2.3", "9.9.9"))
|
|
git(repo, "add", "pyproject.toml")
|
|
git(repo, "commit", "-m", "unreviewed replacement tree")
|
|
git(repo, "replace", tagged_commit, git_text(repo, "rev-parse", "HEAD"))
|
|
with patch.dict(os.environ, {"GIT_DIR": str(self.root / "outside"), "PATH": "/outside/bin"}):
|
|
with materialized_source_tree(repo, source_ref="v1.2.3") as source:
|
|
self.assertEqual(original, (source / "pyproject.toml").read_text())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|