Harden source release preparation and record verified security follow-up
Dependency Audit / dependency-audit (push) Successful in 1m51s
Deployment Installer / deployment-installer (push) Successful in 8s
Security Audit / security-audit (push) Successful in 12m32s

This commit is contained in:
2026-09-08 08:04:12 +02:00
parent 9554657bb5
commit 58d320d9b3
26 changed files with 3833 additions and 163 deletions
+23
View File
@@ -780,6 +780,11 @@ class DeploymentInstallerTests(unittest.TestCase):
self.assertIn("redis", compose["services"])
self.assertIn("worker", compose["services"])
self.assertIn("load-balancer", compose["services"])
self.assertEqual(
"haproxy:3.2.23-alpine@sha256:"
"6343ce34a132a5dceaa24767d739df2bd519f8f7c1079ae39e4821334e8eb42e",
compose["services"]["load-balancer"]["image"],
)
self.assertNotIn("test-mail", compose["services"])
self.assertEqual(
["127.0.0.1:8080:8080"],
@@ -1145,8 +1150,26 @@ class DeploymentInstallerTests(unittest.TestCase):
self.assertEqual(1, parsed.replicas.web)
self.assertEqual(1, parsed.replicas.worker)
self.assertEqual("managed", parsed.components.load_balancer.mode)
self.assertEqual(
default_spec().components.load_balancer.image,
parsed.components.load_balancer.image,
)
self.assertEqual("local", parsed.ingress.mode)
def test_load_balancer_patch_does_not_rewrite_an_existing_image(self) -> None:
for image in (
"haproxy:3.2.21-alpine",
"registry.example.test/haproxy@sha256:" + "a" * 64,
):
with self.subTest(image=image):
saved = default_spec(load_balancer_image=image).to_dict()
restored = parse_spec(json.loads(json.dumps(saved)))
compose = render_compose(restored)
self.assertEqual(image, restored.components.load_balancer.image)
self.assertEqual(image, compose["services"]["load-balancer"]["image"])
def test_compose_contains_no_secret_values(self) -> None:
spec = default_spec()
values = initial_secrets(spec)
+240
View File
@@ -0,0 +1,240 @@
"""Local mixed-owner admission/recovery evidence, not production capacity certification.
All inputs are synthetic and held in memory. The spawn observer temporarily
holds the admitted parent's handshake so the other owners encounter the same
occupied slot deterministically. Children perform real XLSX, template and
Dataflow work; there is no mocked process execution, database, or live service.
The non-queuing gate promises retryable rejection, not scheduler fairness.
"""
from __future__ import annotations
from concurrent.futures import ThreadPoolExecutor
from io import BytesIO
import json
import os
import threading
import time
from types import SimpleNamespace
import unittest
from unittest.mock import patch
try:
from openpyxl import Workbook
from govoplan_connectors.backend.tabular_adapters import (
parse_managed_tabular_content,
)
from govoplan_core.core.templates import TemplateRenderRequest
from govoplan_core.security import bounded_process
from govoplan_core.security.bounded_process import ProcessBudgetError
from govoplan_core.settings import settings
from govoplan_dataflow.backend.backends import execute_typed_graph
from govoplan_dataflow.backend.schemas import (
GraphEdge,
GraphNode,
GraphPosition,
PipelineGraph,
)
from govoplan_templates.backend.rendering import _render_payload
except ImportError as exc:
raise unittest.SkipTest(
"Mixed-owner isolation requires the optional module test environment."
) from exc
class IsolatedWorkCompositionTests(unittest.TestCase):
def setUp(self) -> None:
workbook = Workbook()
workbook.active.append(["name"])
workbook.active.append(["Ada"])
stream = BytesIO()
workbook.save(stream)
workbook.close()
self.workbook = stream.getvalue()
self.graph = PipelineGraph(
nodes=[
GraphNode(
id="source",
type="source.inline",
label="Source",
position=GraphPosition(x=0, y=0),
config={"source_name": "records", "rows": [{"name": "Ada"}]},
),
GraphNode(
id="output",
type="output",
label="Output",
position=GraphPosition(x=100, y=0),
config={},
),
],
edges=[GraphEdge(id="edge", source="source", target="output")],
)
def xlsx(self):
rows, sheet = parse_managed_tabular_content(
self.workbook,
filename="synthetic.xlsx",
content_type=None,
delimiter=",",
sheet_name=None,
)
self.assertEqual(rows, ({"name": "Ada"},))
self.assertEqual(sheet, "Sheet")
return "xlsx"
def templates(self):
payload, content_type, _pages = _render_payload(
SimpleNamespace(name="Synthetic template"),
SimpleNamespace(
content_text="Hello {{item.name}}",
content_html=None,
template_type="letter",
layout={},
output_profiles=[],
),
request=TemplateRenderRequest(
template_id="synthetic", output_format="text"
),
items=({"name": "Ada"},),
)
self.assertEqual(payload, b"Hello Ada")
self.assertEqual(content_type, "text/plain; charset=utf-8")
return "templates"
def dataflow(self):
result = execute_typed_graph(self.graph, backend="reference")
self.assertEqual(result.rows, [{"name": "Ada"}])
return "dataflow"
def test_one_shared_slot_rejects_other_owners_and_all_retries_recover(self):
owners = {
"xlsx": self.xlsx,
"templates": self.templates,
"dataflow": self.dataflow,
}
processes = []
modules = []
hold_next = False
entered = threading.Event()
release = threading.Event()
observer_lock = threading.Lock()
maximum_unreaped = 0
observer_timeouts = 0
original_popen = bounded_process.subprocess.Popen
def observe_spawn(*args, **kwargs):
nonlocal hold_next, maximum_unreaped, observer_timeouts
process = original_popen(*args, **kwargs)
with observer_lock:
processes.append(process)
modules.append(args[0][5])
maximum_unreaped = max(
maximum_unreaped, sum(item.returncode is None for item in processes)
)
should_hold = hold_next
hold_next = False
if should_hold:
entered.set()
if not release.wait(8):
# Return control so the real runner's normal timeout and
# process-group cleanup still own this child on test error.
observer_timeouts += 1
return process
def rejected(operation):
try:
operation()
except Exception as exc:
cause = exc
while cause is not None and not isinstance(cause, ProcessBudgetError):
cause = cause.__cause__
self.assertIsInstance(cause, ProcessBudgetError)
self.assertEqual(cause.code, "busy")
return "busy"
self.fail(
"A different module admitted work while the shared slot was occupied."
)
started = time.monotonic()
busy_count = 0
try:
with (
patch.object(settings, "isolated_process_concurrency", 1),
patch.object(bounded_process.subprocess, "Popen", observe_spawn),
ThreadPoolExecutor(max_workers=3) as executor,
):
for owner, operation in owners.items():
with self.subTest(admitted_owner=owner):
entered.clear()
release.clear()
hold_next = True
holder = executor.submit(operation)
try:
self.assertTrue(
entered.wait(5),
"The admitted operation never spawned its real child.",
)
children_before = len(processes)
others = [
work for label, work in owners.items() if label != owner
]
denied = [
executor.submit(rejected, work) for work in others
]
self.assertEqual(
[future.result(timeout=5) for future in denied],
["busy", "busy"],
)
busy_count += len(denied)
self.assertEqual(len(processes), children_before)
finally:
release.set()
self.assertEqual(holder.result(timeout=15), owner)
self.assertEqual(bounded_process._active, 0)
# Every rejected owner is retried through its real API.
# Each must complete after the previous holder releases.
for other in others:
other()
self.assertEqual(bounded_process._active, 0)
finally:
release.set()
for process in processes:
self.assertIsNotNone(process.returncode, "Worker was not reaped.")
self.assertTrue(
all(
stream.closed
for stream in (process.stdin, process.stdout, process.stderr)
)
)
with self.assertRaises(ChildProcessError):
os.waitpid(process.pid, os.WNOHANG)
self.assertEqual(maximum_unreaped, 1)
self.assertEqual(observer_timeouts, 0)
self.assertEqual(len(processes), 9)
self.assertEqual(busy_count, 6)
self.assertEqual(
set(modules),
{
"govoplan_connectors.backend.tabular_adapters",
"govoplan_templates.backend.rendering",
"govoplan_dataflow.backend.backends.reference",
},
)
print(
json.dumps(
{
"local_composition": {
"successful_children": len(processes),
"busy_rejections": busy_count,
"maximum_unreaped_children": maximum_unreaped,
"all_children_reaped": True,
"seconds": round(time.monotonic() - started, 3),
}
}
)
)
if __name__ == "__main__":
unittest.main()
@@ -96,6 +96,18 @@ class PlatformInterfaceInventoryTests(unittest.TestCase):
with self.assertRaisesRegex(ValueError, "tracking_issue"):
inventory._load_endpoint_declarations(path)
def test_bounded_workflow_read_apis_have_explicit_headless_declarations(self) -> None:
declarations = inventory._load_endpoint_declarations(inventory.DEFAULT_ENDPOINT_DECLARATIONS)
for path in (
"/workflow/instances/summaries", "/workflow/instances/{}/summary",
"/workflow/instances/{}/steps", "/workflow/instances/{}/events",
):
with self.subTest(path=path):
entry = declarations[("govoplan-workflow-engine", "GET", path)]
self.assertEqual("intentionally_headless", entry["category"])
self.assertIn("current-authorized", entry["rationale"])
self.assertIn("workflow.instance-history", entry["rationale"])
def test_inventory_reports_unclassified_and_stale_endpoint_declarations(
self,
) -> None:
+330
View File
@@ -0,0 +1,330 @@
from __future__ import annotations
from dataclasses import replace
from contextlib import redirect_stdout
import io
import json
from pathlib import Path
import runpy
import shutil
import sys
import unittest
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "tools/release"))
from govoplan_release import meta_preparation # noqa: E402
from govoplan_release.git_state import collect_repository_snapshot # noqa: E402
from govoplan_release.meta_preparation import ( # noqa: E402
MetaPreparationError,
prepare_developer_meta_package,
)
from govoplan_release.model import RepositorySpec # noqa: E402
from govoplan_release.selective_planner import build_selective_release_plan # noqa: E402
from govoplan_release.version_metadata import ( # noqa: E402
VersionMetadataError,
apply_version_metadata_mutations,
version_metadata_mutations,
)
import test_release_meta_source_tag as meta_fixture # noqa: E402
from test_release_plan_guidance import dashboard # noqa: E402
from test_release_repository_tag import create_release_repo, git, git_text # noqa: E402
class MetaPreparationTests(unittest.TestCase):
synchronize = meta_fixture.MetaSourceTagTests.synchronize
def setUp(self):
meta_fixture.MetaSourceTagTests.setUp(self)
self.operator = self.root / "operator"
self.generator = (
self.operator / "tools/release/generate-developer-meta-package.py"
)
self.generator.parent.mkdir(parents=True)
shutil.copyfile(
ROOT / "tools/release/generate-developer-meta-package.py", self.generator
)
self.enterContext(patch.object(meta_preparation, "META_ROOT", self.operator))
def prepare_core(self):
apply_version_metadata_mutations(self.core, target_version="0.1.11")
git(self.core, "add", ".")
git(self.core, "commit", "-m", "Prepared synthetic Core target")
def preview(self, **kwargs):
return prepare_developer_meta_package(
repo_path=self.meta, target_version="0.1.11", **kwargs
)
def apply(self, preview):
return self.preview(
apply=True, expected_receipt=preview["receipt"], confirm_out_of_run=True
)
def test_full_canonical_preview_apply_and_shared_mutation_discovery(self):
extra, remote = create_release_repo(
root=self.root,
workspace=self.workspace,
name="govoplan-workflow-engine",
version="0.2.3",
)
self.specs.append(
{
"name": extra.name,
"path": extra.name,
"category": "module",
"subtype": "",
"remote": str(remote),
}
)
self.registry.write_text(json.dumps({"repositories": self.specs}))
self.prepare_core()
before = self.package.read_bytes()
preview = self.preview()
self.assertEqual("planned", preview["status"])
self.assertEqual(before, self.package.read_bytes())
mutations = version_metadata_mutations(self.meta, target_version="0.1.11")
self.assertEqual([meta_preparation.PACKAGE], [item.path for item in mutations])
self.assertIn(b"govoplan-workflow-engine==0.2.3", mutations[0].after)
self.assertIn(b"govoplan-core==0.1.11", mutations[0].after)
result = self.apply(preview)
self.assertEqual("prepared", result["status"])
self.assertEqual(mutations[0].after, self.package.read_bytes())
self.assertEqual(
self.render(workspace=self.workspace, requirements=self.requirements),
self.package.read_text(),
)
self.assertEqual(
f"M {meta_preparation.PACKAGE}",
git_text(self.meta, "status", "--porcelain"),
)
self.assertFalse(git_text(self.meta, "tag", "--list"))
def test_core_target_must_already_be_prepared(self):
before = self.package.read_bytes()
with self.assertRaisesRegex(MetaPreparationError, "Prepare and commit Core"):
self.preview()
self.assertEqual(before, self.package.read_bytes())
def test_changed_requirements_receipt_blocks_before_any_output(self):
self.prepare_core()
preview = self.preview()
before = self.package.read_bytes()
self.requirements.write_text(
self.requirements.read_text() + "# reviewed different inputs\n"
)
git(self.meta, "add", ".")
git(self.meta, "commit", "-m", "Changed synthetic requirements")
with self.assertRaisesRegex(MetaPreparationError, "changed since"):
self.apply(preview)
self.assertEqual(before, self.package.read_bytes())
def test_source_change_immediately_before_effect_is_rechecked(self):
self.prepare_core()
preview = self.preview()
before = self.package.read_bytes()
original = meta_preparation.preview_meta_mutation
def changed(**kwargs):
result = original(**kwargs)
self.requirements.write_text(
self.requirements.read_text() + "# concurrent change\n"
)
git(self.meta, "add", ".")
git(self.meta, "commit", "-m", "Concurrent synthetic change")
return result
with patch.object(
meta_preparation, "preview_meta_mutation", side_effect=changed
):
with self.assertRaisesRegex(MetaPreparationError, "changed before"):
self.apply(preview)
self.assertEqual(before, self.package.read_bytes())
def test_core_full_package_and_operator_generator_are_receipt_bound(self):
self.prepare_core()
for path, repository in (
(self.core / "pyproject.toml", self.core),
(self.access / "pyproject.toml", self.access),
(self.generator, None),
):
with self.subTest(input=path.name, repo=str(repository)):
preview = self.preview()
before = self.package.read_bytes()
path.write_text(path.read_text() + "\n# changed frozen input\n")
if repository is not None:
git(repository, "add", ".")
git(
repository,
"commit",
"-m",
"Changed synthetic composition input",
)
with self.assertRaisesRegex(MetaPreparationError, "changed since"):
self.apply(preview)
self.assertEqual(before, self.package.read_bytes())
def test_post_write_source_change_is_reported_without_retry_or_rollback(self):
from govoplan_release import version_metadata
self.prepare_core()
preview = self.preview()
original = version_metadata._atomic_write
def changed(path, payload):
original(path, payload)
self.requirements.write_text(
self.requirements.read_text() + "# concurrent after write\n"
)
with patch.object(
version_metadata, "_atomic_write", side_effect=changed
) as writer:
with self.assertRaisesRegex(
meta_preparation.MetaPreparationAmbiguous, "write/post-check failed"
):
self.apply(preview)
self.assertEqual(1, writer.call_count)
self.assertIn('version = "0.1.11"', self.package.read_text())
self.assertIn("# concurrent after write", self.requirements.read_text())
def test_write_failure_after_replace_requires_reconciliation(self):
from govoplan_release import version_metadata
self.prepare_core()
preview = self.preview()
original = version_metadata._atomic_write
def partial(path, payload):
original(path, payload)
raise OSError("Synthetic directory fsync failure after replacement")
with patch.object(version_metadata, "_atomic_write", side_effect=partial) as writer:
with self.assertRaisesRegex(meta_preparation.MetaPreparationAmbiguous, "may have been written"):
self.apply(preview)
self.assertEqual(1, writer.call_count)
self.assertIn('version = "0.1.11"', self.package.read_text())
def test_cli_requires_reviewed_receipt_and_explicit_out_of_run_confirmation(self):
self.prepare_core()
main = runpy.run_path(
str(ROOT / "tools/release/prepare-developer-meta-package.py")
)["main"]
arguments = [
"prepare-developer-meta-package.py",
"--workspace",
str(self.workspace),
"--target-version",
"0.1.11",
]
output = io.StringIO()
with patch.object(sys, "argv", arguments), redirect_stdout(output):
self.assertEqual(0, main())
preview = self.root / "meta-preview.json"
preview.write_text(output.getvalue())
with (
patch.object(sys, "argv", [*arguments, "--apply"]),
redirect_stdout(io.StringIO()),
):
self.assertEqual(1, main())
with (
patch.object(
sys,
"argv",
[
*arguments,
"--apply",
"--receipt",
str(preview),
"--confirm-out-of-run",
],
),
redirect_stdout(io.StringIO()),
):
self.assertEqual(0, main())
def test_unknown_full_input_and_unsafe_operator_tooling_fail_closed(self):
self.prepare_core()
unknown = self.workspace / "govoplan-unknown/pyproject.toml"
unknown.parent.mkdir()
unknown.write_text('[project]\nname="govoplan-unknown"\nversion="1.0.0"\n')
with self.assertRaisesRegex(MetaPreparationError, "unregistered"):
self.preview()
unknown.unlink()
self.generator.chmod(0o666)
with self.assertRaisesRegex(MetaPreparationError, "owned, bounded regular"):
self.preview()
def test_wrong_nested_identity_and_existing_immutable_tag_fail_closed(self):
self.prepare_core()
original = self.package.read_text()
self.package.write_text(
original.replace('name = "govoplan"', 'name = "not-govoplan"')
)
git(self.meta, "add", ".")
git(self.meta, "commit", "-m", "Wrong synthetic package identity")
with self.assertRaisesRegex(MetaPreparationError, "identity"):
self.preview()
self.package.write_text(original)
git(self.meta, "add", ".")
git(self.meta, "commit", "-m", "Restore synthetic package identity")
git(self.meta, "tag", "-a", "v0.1.11", "-m", "Immutable target")
with self.assertRaisesRegex(MetaPreparationError, "target Meta tag"):
self.preview()
def test_no_generic_durable_self_mutation_or_running_tooling_target(self):
self.prepare_core()
preview = self.preview()
with self.assertRaisesRegex(VersionMetadataError, "outside durable runs"):
apply_version_metadata_mutations(self.meta, target_version="0.1.11")
with self.assertRaisesRegex(MetaPreparationError, "confirm"):
self.preview(apply=True, expected_receipt=preview["receipt"])
with patch.object(meta_preparation, "META_ROOT", self.meta):
with self.assertRaisesRegex(MetaPreparationError, "running operator"):
self.preview()
def test_next_version_plan_is_actionable_core_first_without_meta_executor(self):
snapshots = tuple(
collect_repository_snapshot(
RepositorySpec(**spec),
workspace_root=self.workspace,
target_tag="v0.1.11",
)
for spec in self.specs[:2]
)
source = replace(
dashboard(workspace=self.workspace, version=self.version),
repositories=snapshots,
)
plan = build_selective_release_plan(
source,
selected_repos=("govoplan", "govoplan-core"),
target_version="0.1.11",
)
self.assertEqual(
["govoplan-core", "govoplan"], [unit.repo for unit in plan.units]
)
findings = [
finding for finding in plan.gate_findings if finding.repo == "govoplan"
]
self.assertEqual(
["developer_meta_core_preparation_required"],
[finding.code for finding in findings],
)
self.assertIn("prepare-developer-meta-package.py", findings[0].remediation)
meta_steps = [step for step in plan.dry_run_steps if step.repo == "govoplan"]
self.assertEqual(
["govoplan:prepare-support", "govoplan:publish-support"],
[step.id for step in meta_steps],
)
self.assertTrue(all(step.status == "needs-executor" for step in meta_steps))
self.prepare_core()
prepared = build_selective_release_plan(
source, selected_repos=("govoplan",), target_version="0.1.11"
)
self.assertEqual("developer_meta_out_of_run", prepared.gate_findings[0].code)
if __name__ == "__main__":
unittest.main()
+569
View File
@@ -0,0 +1,569 @@
from __future__ import annotations
import json
import os
from pathlib import Path
import runpy
import shutil
import subprocess
import sys
import tempfile
import unittest
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "tools/release"))
from govoplan_release import source_tag_batch as meta_source_tag, workspace # noqa: E402
from govoplan_release.git_state import collect_versions # noqa: E402
from govoplan_release.model import RepositorySnapshot, RepositorySpec, VersionSnapshot # noqa: E402
from govoplan_release.repository_tag import tag_repositories # noqa: E402
from govoplan_release.selective_planner import build_unit # noqa: E402
from govoplan_release.version_alignment import repository_version_issues # noqa: E402
from test_release_repository_tag import ( # noqa: E402
add_scoped_workflow_manifest,
create_release_repo,
git,
git_text,
ref_exists,
)
class MetaSourceTagTests(unittest.TestCase):
def setUp(self):
self.temporary = self.enterContext(
tempfile.TemporaryDirectory(prefix="meta-release-tests-")
)
self.root = Path(self.temporary)
self.workspace = self.root / "workspace"
self.workspace.mkdir()
self.version = "0.1.10"
self.core, self.core_remote = create_release_repo(
root=self.root,
workspace=self.workspace,
name="govoplan-core",
version=self.version,
)
self.access, self.access_remote = create_release_repo(
root=self.root,
workspace=self.workspace,
name="govoplan-access",
version=self.version,
)
add_scoped_workflow_manifest(self.access)
self.meta = self.workspace / "govoplan"
self.meta_remote = self.root / "govoplan.git"
git(self.root, "init", "--bare", str(self.meta_remote))
git(self.workspace, "init", "-b", "main", str(self.meta))
git(self.meta, "config", "user.name", "Meta Release Fixture")
git(self.meta, "config", "user.email", "release@example.invalid")
self.package = self.meta / "packages/govoplan-meta/pyproject.toml"
self.package.parent.mkdir(parents=True)
self.requirements = self.meta / "requirements-release.txt"
self.requirements.write_text(
"../govoplan-core\ngovoplan-access @ git+ssh://git@example.invalid/GovOPlaN/govoplan-access.git@v0.1.10\n"
)
self.render = runpy.run_path(
str(ROOT / "tools/release/generate-developer-meta-package.py")
)["render"]
self.synchronize()
git(self.meta, "add", ".")
git(self.meta, "commit", "-m", "Nested developer package")
git(self.meta, "remote", "add", "origin", str(self.meta_remote))
git(self.meta, "push", "-u", "origin", "main")
self.specs = [
{
"name": name,
"category": "system" if subtype else "module",
"subtype": subtype,
"path": name,
"remote": str(remote),
}
for name, subtype, remote in (
("govoplan", "meta", self.meta_remote),
("govoplan-core", "kernel", self.core_remote),
("govoplan-access", "", self.access_remote),
)
]
self.registry = self.root / "repositories.json"
self.registry.write_text(json.dumps({"repositories": self.specs}))
self.enterContext(patch.object(workspace, "REPOSITORIES_FILE", self.registry))
def synchronize(self):
self.package.write_text(
self.render(workspace=self.workspace, requirements=self.requirements)
)
def commit_meta(self):
git(self.meta, "add", ".")
git(self.meta, "commit", "-m", "Changed synthetic metadata")
def tag(
self,
*,
repos=("govoplan", "govoplan-core"),
apply=False,
push=False,
**overrides,
):
return tag_repositories(
repos=repos,
repo_versions={repo: self.version for repo in repos},
workspace_root=self.workspace,
apply=apply,
push=push,
**overrides,
)
def assert_no_tags(self):
for repo in (
self.meta,
self.meta_remote,
self.core,
self.core_remote,
self.access,
self.access_remote,
):
self.assertFalse(ref_exists(repo, "refs/tags/v0.1.10"), str(repo))
def test_explicit_nested_version_collection_and_alignment_without_root_package(
self,
):
versions = collect_versions(self.meta)
self.assertIsNone(versions.pyproject)
self.assertEqual(self.version, versions.developer_meta)
self.assertEqual(self.version, versions.primary)
self.assertFalse((self.meta / "pyproject.toml").exists())
self.assertEqual(
(), repository_version_issues(self.meta, expected_version=self.version)
)
mismatch = repository_version_issues(self.meta, expected_version="0.1.11")
self.assertTrue(
any(
issue.source == "packages/govoplan-meta/pyproject.toml"
for issue in mismatch
)
)
def test_planner_and_console_display_the_explicit_nested_version(self):
snapshot = RepositorySnapshot(
spec=RepositorySpec(**self.specs[0]),
absolute_path=str(self.meta),
exists=True,
is_git=True,
has_head=True,
branch="main",
versions=VersionSnapshot(developer_meta=self.version),
)
unit = build_unit(snapshot, target_version=None, contracts=None)
self.assertEqual(self.version, unit.current_version)
self.assertEqual(self.version, unit.target_version)
html = (ROOT / "tools/release/webui/index.html").read_text()
self.assertIn(
"if (versions.developer_meta) return versions.developer_meta;", html
)
drift = RepositorySnapshot(
spec=snapshot.spec,
absolute_path=str(self.meta),
exists=True,
is_git=True,
has_head=True,
branch="main",
versions=VersionSnapshot(pyproject="0.1.9", developer_meta=self.version),
)
self.assertTrue(
any(
"version metadata is not aligned" in item
for item in build_unit(
drift, target_version=self.version, contracts=None
).blockers
)
)
def test_unknown_nested_package_and_missing_or_wrong_meta_identity_fail_closed(
self,
):
unknown = self.workspace / "unknown"
nested = unknown / "packages/govoplan-meta/pyproject.toml"
nested.parent.mkdir(parents=True)
nested.write_text(self.package.read_text())
self.assertIsNone(collect_versions(unknown).primary)
self.assertIn(
"no version metadata",
repository_version_issues(unknown, expected_version=self.version)[
0
].message,
)
for value in ("", '[project]\nname="not-govoplan"\nversion="0.1.10"\n'):
with self.subTest(value=value):
self.package.write_text(value)
self.assertTrue(
repository_version_issues(self.meta, expected_version=self.version)
)
def test_preview_local_tag_and_publish_share_complete_nested_contract(self):
preview = self.tag(push=True)
self.assertEqual("planned", preview["status"], preview)
self.assertEqual(
["govoplan-core", "govoplan"],
[row["repo"] for row in preview["repositories"]],
)
self.assertEqual("registered-meta-batch-v1", preview["source_contract"])
self.assert_no_tags()
local = self.tag(apply=True)
self.assertEqual("tagged", local["status"], local)
for repo in (self.core, self.meta):
self.assertEqual(
"tag", git_text(repo, "cat-file", "-t", "refs/tags/v0.1.10")
)
self.assertFalse(ref_exists(self.meta_remote, "refs/tags/v0.1.10"))
published = self.tag(apply=True, push=True)
self.assertEqual("published", published["status"], published)
for repo, remote in (
(self.core, self.core_remote),
(self.meta, self.meta_remote),
):
self.assertEqual(
git_text(repo, "rev-parse", "HEAD"),
git_text(remote, "rev-parse", "refs/heads/main"),
)
self.assertEqual(
git_text(repo, "rev-parse", "refs/tags/v0.1.10"),
git_text(remote, "rev-parse", "refs/tags/v0.1.10"),
)
again = self.tag(apply=True, push=True)
self.assertEqual("published", again["status"], again)
def test_stale_composition_blocks_whole_batch_before_local_tag_or_push(self):
self.package.write_text(
self.package.read_text().replace(
"govoplan-access==0.1.10", "govoplan-access==0.1.9"
)
)
self.commit_meta()
for apply, push in ((False, False), (True, False), (True, True)):
result = self.tag(
repos=("govoplan-core", "govoplan-access", "govoplan"),
apply=apply,
push=push,
)
self.assertEqual("blocked", result["status"], result)
self.assert_no_tags()
def test_core_outside_batch_requires_matching_existing_and_published_tag(self):
self.assertEqual("blocked", self.tag(repos=("govoplan",))["status"])
git(self.core, "tag", "-a", "v0.1.10", "-m", "Core release")
self.assertEqual("planned", self.tag(repos=("govoplan",))["status"])
self.assertEqual("blocked", self.tag(repos=("govoplan",), push=True)["status"])
git(self.core, "push", "origin", "refs/tags/v0.1.10")
self.assertEqual("planned", self.tag(repos=("govoplan",), push=True)["status"])
def test_changed_core_version_and_explicit_selected_version_mismatch_block(self):
mismatch = tag_repositories(
repos=("govoplan", "govoplan-core"),
repo_versions={"govoplan": self.version, "govoplan-core": "0.1.11"},
workspace_root=self.workspace,
apply=True,
push=True,
)
self.assertEqual("blocked", mismatch["status"], mismatch)
(self.core / "pyproject.toml").write_text(
'[project]\nname="govoplan-core"\nversion="0.1.11"\n'
)
git(self.core, "add", ".")
git(self.core, "commit", "-m", "Core new version")
result = self.tag(apply=True, push=True)
self.assertEqual("blocked", result["status"], result)
self.assert_no_tags()
def test_unsafe_origin_on_any_selected_repo_blocks_every_effect(self):
for repo in (self.meta, self.core, self.access):
with self.subTest(repo=repo.name):
git(
repo,
"config",
"remote.origin.pushurl",
str(self.root / "unregistered.git"),
)
result = self.tag(
repos=("govoplan-core", "govoplan-access", "govoplan"),
apply=True,
push=True,
)
self.assertEqual("blocked", result["status"], result)
self.assertIn("registered origin", result["repositories"][0]["detail"])
self.assert_no_tags()
git(repo, "config", "--unset", "remote.origin.pushurl")
def test_world_writable_nonsticky_parent_blocks_without_changing_permissions(self):
original = self.root.stat().st_mode & 0o7777
self.root.chmod(0o777)
try:
for apply in (False, True):
result = self.tag(apply=apply, push=True)
self.assertEqual("blocked", result["status"], result)
self.assertIn(
"group/world writable", result["repositories"][0]["detail"]
)
self.assertEqual(0o777, self.root.stat().st_mode & 0o7777)
self.assert_no_tags()
finally:
self.root.chmod(original)
def test_wrong_metadata_owner_blocks_before_remote_lookup(self):
config = self.meta / ".git/config"
original = Path.lstat
def wrong_owner(path, *args, **kwargs):
observed = original(path, *args, **kwargs)
if path == config:
fields = list(observed)
fields[4] = os.geteuid() + 1
return os.stat_result(fields)
return observed
with (
patch.object(Path, "lstat", new=wrong_owner),
patch.object(
meta_source_tag,
"registered_source_origin_issues",
side_effect=AssertionError("must validate ownership before Git"),
),
):
result = self.tag(apply=True, push=True)
self.assertEqual("blocked", result["status"], result)
self.assertIn("current operator", result["repositories"][0]["detail"])
self.assert_no_tags()
def test_hidden_index_flags_cannot_disguise_modified_release_metadata(self):
for flag, undo in (
("--assume-unchanged", "--no-assume-unchanged"),
("--skip-worktree", "--no-skip-worktree"),
):
for repo, relative in (
(self.meta, "packages/govoplan-meta/pyproject.toml"),
(self.core, "pyproject.toml"),
):
with self.subTest(flag=flag, repo=repo.name):
target = repo / relative
original = target.read_text()
git(repo, "update-index", flag, relative)
target.write_text(
original
+ "\n# Hidden working-tree input differs from frozen HEAD\n"
)
try:
self.assertEqual("", git_text(repo, "status", "--porcelain"))
result = self.tag(apply=True, push=True)
self.assertEqual("blocked", result["status"], result)
self.assertIn(
"index entries", result["repositories"][0]["detail"]
)
self.assert_no_tags()
finally:
target.write_text(original)
git(repo, "update-index", undo, relative)
def test_read_only_git_target_is_not_repaired_or_tagged(self):
metadata = self.meta / ".git"
original = metadata.stat().st_mode & 0o7777
metadata.chmod(0o500)
try:
result = self.tag(apply=True, push=True)
self.assertEqual("blocked", result["status"], result)
self.assertEqual(0o500, metadata.stat().st_mode & 0o7777)
self.assert_no_tags()
finally:
metadata.chmod(original)
def test_git_object_alternates_are_rejected_before_remote_lookup(self):
(self.meta / ".git/objects/info/alternates").write_text(
str(self.root / "outside-objects") + "\n"
)
with patch.object(
meta_source_tag,
"registered_source_origin_issues",
side_effect=AssertionError("must reject alternates before Git"),
):
result = self.tag(apply=True, push=True)
self.assertEqual("blocked", result["status"], result)
self.assertIn("alternates", result["repositories"][0]["detail"])
self.assert_no_tags()
def test_owned_worktree_metadata_inside_private_workspace_is_supported(self):
main_checkout = self.workspace / "meta-main-storage"
self.meta.rename(main_checkout)
git(main_checkout, "worktree", "add", "--force", str(self.meta), "main")
self.assertTrue((self.meta / ".git").is_file())
result = self.tag(apply=True)
self.assertEqual("tagged", result["status"], result)
filesystem = result["source_receipts"]["govoplan"]["filesystem"]
self.assertEqual(
str(main_checkout / ".git"), filesystem["git_common_directory"][0]
)
def test_git_directory_replacement_with_same_head_changes_frozen_receipt(self):
preview = meta_source_tag._preview_repositories
def swapped_git_directory(**kwargs):
result = preview(**kwargs)
original = self.meta / ".git"
backup = self.root / "original-meta-git"
original.rename(backup)
shutil.copytree(backup, original)
return result
with patch.object(
meta_source_tag,
"_preview_repositories",
side_effect=swapped_git_directory,
):
result = self.tag(apply=True, push=True)
self.assertEqual("blocked", result["status"], result)
self.assertIn("receipt changed", result["repositories"][0]["detail"])
self.assert_no_tags()
def test_non_main_and_divergent_live_main_fail_even_with_stale_tracking(self):
git(self.meta, "switch", "-c", "feature")
self.assertEqual("blocked", self.tag(apply=True)["status"])
git(self.meta, "switch", "main")
clone = self.root / "other-writer"
git(self.root, "clone", "--branch", "main", str(self.meta_remote), str(clone))
git(clone, "config", "user.name", "Other synthetic writer")
git(clone, "config", "user.email", "other@example.invalid")
(clone / "other.txt").write_text("remote divergence\n")
git(clone, "add", ".")
git(clone, "commit", "-m", "Remote main advanced")
git(clone, "push", "origin", "main")
result = self.tag(apply=True, push=True)
self.assertEqual("blocked", result["status"], result)
self.assertIn("live origin/main", result["repositories"][0]["detail"])
self.assert_no_tags()
def test_symlink_checkout_is_not_a_registered_source(self):
original = self.workspace / "moved-meta"
self.meta.rename(original)
self.meta.symlink_to(original, target_is_directory=True)
result = self.tag(apply=True)
self.assertEqual("blocked", result["status"], result)
self.assertIn("symlink", result["repositories"][0]["detail"])
self.assert_no_tags()
def test_lightweight_and_conflicting_annotated_tags_block(self):
git(self.meta, "tag", "v0.1.10")
self.assertEqual("blocked", self.tag(apply=True)["status"])
git(self.meta, "tag", "-d", "v0.1.10")
git(self.meta, "tag", "-a", "v0.1.10", "-m", "First annotation")
git(self.meta, "push", "origin", "refs/tags/v0.1.10")
git(self.meta, "tag", "-d", "v0.1.10")
git(self.meta, "tag", "-a", "v0.1.10", "-m", "Different annotation")
self.assertEqual("blocked", self.tag(apply=True, push=True)["status"])
self.assertFalse(ref_exists(self.core, "refs/tags/v0.1.10"))
def test_meta_source_receipt_changed_after_preflight_blocks_before_first_effect(
self,
):
preview = meta_source_tag._preview_repositories
def changed_after_preflight(**kwargs):
result = preview(**kwargs)
(self.meta / "new-review.txt").write_text("changed after preflight\n")
self.commit_meta()
return result
with patch.object(
meta_source_tag,
"_preview_repositories",
side_effect=changed_after_preflight,
):
result = self.tag(apply=True, push=True)
self.assertEqual("blocked", result["status"], result)
self.assertIn("receipt changed", result["repositories"][0]["detail"])
self.assert_no_tags()
def test_fabricated_push_success_without_remote_receipt_fails_and_stops_batch(self):
original = meta_source_tag.run
def run(command, **kwargs):
if command[:3] == ("git", "push", "--atomic"):
return subprocess.CompletedProcess(command, 0, "", "")
return original(command, **kwargs)
with patch.object(meta_source_tag, "run", side_effect=run):
result = self.tag(apply=True, push=True)
self.assertEqual("partial", result["status"], result)
self.assertEqual("failed", result["repositories"][0]["status"])
self.assertEqual("skipped", result["repositories"][1]["status"])
self.assertFalse(ref_exists(self.meta, "refs/tags/v0.1.10"))
self.assertFalse(ref_exists(self.core_remote, "refs/tags/v0.1.10"))
def test_remote_tag_without_expected_main_receipt_is_not_success(self):
(self.core / "reviewed-change.txt").write_text("release source changes\n")
git(self.core, "add", ".")
git(self.core, "commit", "-m", "Advance reviewed Core source")
original = meta_source_tag.run
pushes = []
def tag_only(command, **kwargs):
if command[:3] == ("git", "push", "--atomic"):
pushes.append(command)
# Simulate a defective transport that claims atomic success,
# while publishing only the exact expected annotation object.
return original(("git", "push", "origin", command[-1]), **kwargs)
return original(command, **kwargs)
with patch.object(meta_source_tag, "run", side_effect=tag_only):
result = self.tag(apply=True, push=True)
self.assertEqual("partial", result["status"], result)
self.assertEqual(1, len(pushes))
self.assertIn("receipt changed", result["repositories"][0]["detail"])
self.assertEqual(
git_text(self.core, "rev-parse", "refs/tags/v0.1.10"),
git_text(self.core_remote, "rev-parse", "refs/tags/v0.1.10"),
)
self.assertNotEqual(
git_text(self.core, "rev-parse", "HEAD"),
git_text(self.core_remote, "rev-parse", "refs/heads/main"),
)
self.assertFalse(ref_exists(self.meta, "refs/tags/v0.1.10"))
def test_meta_receipt_is_rechecked_after_an_earlier_successful_publication(self):
original = meta_source_tag.run
def changed_after_core(command, **kwargs):
result = original(command, **kwargs)
if (
command[:3] == ("git", "push", "--atomic")
and kwargs["cwd"] == self.core
):
(self.meta / "changed-review.txt").write_text(
"new Meta source after Core publication\n"
)
self.commit_meta()
return result
with patch.object(meta_source_tag, "run", side_effect=changed_after_core):
result = self.tag(apply=True, push=True)
self.assertEqual("partial", result["status"], result)
self.assertTrue(ref_exists(self.core_remote, "refs/tags/v0.1.10"))
self.assertFalse(ref_exists(self.meta, "refs/tags/v0.1.10"))
self.assertEqual("skipped", result["repositories"][1]["status"])
def test_unknown_selected_repository_cannot_use_meta_support_exception(self):
result = self.tag(repos=("govoplan", "unknown"), apply=True, push=True)
self.assertEqual("blocked", result["status"], result)
self.assertIn("not registered", result["repositories"][0]["detail"])
self.assert_no_tags()
def test_selected_checkout_generator_is_never_executed(self):
malicious = self.meta / "tools/release/generate-developer-meta-package.py"
malicious.parent.mkdir(parents=True)
malicious.write_text(
'raise RuntimeError("selected checkout must not execute")\n'
)
self.commit_meta()
self.assertEqual("planned", self.tag()["status"])
if __name__ == "__main__":
unittest.main()
+14
View File
@@ -6,6 +6,7 @@ import subprocess
import sys
import tempfile
import unittest
from unittest.mock import patch
from pathlib import Path
from fastapi.testclient import TestClient
@@ -17,6 +18,7 @@ if str(RELEASE_ROOT) not in sys.path:
sys.path.insert(0, str(RELEASE_ROOT))
from govoplan_release.repository_tag import tag_repositories # noqa: E402
from govoplan_release import workspace as release_workspace # noqa: E402
from server.app import create_app # noqa: E402
@@ -39,6 +41,14 @@ class ReleaseRepositoryTagTests(unittest.TestCase):
version="0.1.10",
)
add_scoped_workflow_manifest(self.manifest_repo)
# The operator's test catalog explicitly registers known synthetic
# endpoints; production trust checks are not patched or bypassed.
self.registry = self.root / "registered-test-repositories.json"
self.registered = json.loads((META_ROOT / "repositories.json").read_text())
for spec in self.registered["repositories"]:
spec["remote"] = str(self.root / f"{spec['name']}.git")
self.registry.write_text(json.dumps(self.registered))
self.enterContext(patch.object(release_workspace, "REPOSITORIES_FILE", self.registry))
def tearDown(self) -> None:
self.temporary.cleanup()
@@ -128,6 +138,10 @@ class ReleaseRepositoryTagTests(unittest.TestCase):
name="govoplan-core",
version="0.1.10",
)
for spec in self.registered["repositories"]:
if spec["name"] == "govoplan-core":
spec["remote"] = str(remote_root / "govoplan-core.git")
self.registry.write_text(json.dumps(self.registered))
result = tag_repositories(
repos=("govoplan-core",),
+318
View File
@@ -0,0 +1,318 @@
from __future__ import annotations
import json
import os
from pathlib import Path
import subprocess
import sys
import unittest
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "tools/release"))
from govoplan_release import source_tag_batch # noqa: E402
from govoplan_release.repository_tag import tag_repositories # noqa: E402
import test_release_meta_source_tag as meta_fixture # noqa: E402
import test_release_repository_tag as release_fixture # noqa: E402
from test_release_repository_tag import git, git_text, ref_exists # noqa: E402
class RegisteredSourceTagBatchTests(unittest.TestCase):
setUp = meta_fixture.MetaSourceTagTests.setUp
synchronize = meta_fixture.MetaSourceTagTests.synchronize
assert_no_tags = meta_fixture.MetaSourceTagTests.assert_no_tags
def tag(self, *, repos=("govoplan-access",), apply=False, push=False):
return tag_repositories(
repos=repos,
repo_versions={repo: self.version for repo in repos},
workspace_root=self.workspace,
apply=apply,
push=push,
)
def test_python_only_module_publishes_without_meta_or_core_checkout(self):
self.meta.rename(self.root / "unused-meta")
self.core.rename(self.root / "unused-core")
preview = self.tag(push=True)
self.assertEqual("planned", preview["status"], preview)
self.assertEqual("registered-source-batch-v1", preview["source_contract"])
self.assertEqual({}, preview["bundle_input_receipts"])
self.assertFalse(ref_exists(self.access, "refs/tags/v0.1.10"))
published = self.tag(apply=True, push=True)
self.assertEqual("published", published["status"], published)
self.assertEqual(["govoplan-access"], list(published["source_receipts"]))
self.assertEqual(
git_text(self.access, "rev-parse", "HEAD"),
git_text(self.access_remote, "rev-parse", "refs/heads/main"),
)
def test_core_only_batch_has_no_meta_composition_requirement(self):
self.meta.rename(self.root / "unused-meta")
result = self.tag(repos=("govoplan-core",), apply=True, push=True)
self.assertEqual("published", result["status"], result)
self.assertEqual(["govoplan-core"], list(result["source_receipts"]))
def test_backend_only_publication_never_reads_irrelevant_unsafe_core_json(self):
webui = self.core / "webui"
webui.mkdir()
(webui / "package.release.json").write_text("invalid unselected Core JSON")
(webui / "package-lock.release.json").symlink_to(
self.root / "not-a-core-release-lock"
)
from govoplan_release import version_alignment
original = version_alignment._json_object
def read(path):
if path.is_relative_to(webui):
raise AssertionError(
"backend-only publication must not read unrelated Core JSON"
)
return original(path)
with patch.object(version_alignment, "_json_object", side_effect=read):
result = self.tag(apply=True, push=True)
self.assertEqual("published", result["status"], result)
self.assertEqual({}, result["bundle_input_receipts"])
def test_source_origin_parent_and_read_only_target_guards_apply_without_meta(self):
for problem in ("origin", "parent", "read_only"):
with self.subTest(problem=problem):
parent_mode = self.root.stat().st_mode & 0o7777
git_directory = self.access / ".git"
git_mode = git_directory.stat().st_mode & 0o7777
if problem == "origin":
git(
self.access,
"config",
"remote.origin.pushurl",
str(self.root / "unknown.git"),
)
elif problem == "parent":
self.root.chmod(0o777)
else:
git_directory.chmod(0o500)
try:
for apply in (False, True):
result = self.tag(apply=apply, push=True)
self.assertEqual("blocked", result["status"], result)
self.assert_no_tags()
finally:
self.root.chmod(parent_mode)
git_directory.chmod(git_mode)
if problem == "origin":
git(self.access, "config", "--unset", "remote.origin.pushurl")
def test_wrong_owner_symlink_and_hidden_index_are_rejected_without_meta(self):
config = self.access / ".git/config"
original_stat = Path.lstat
def wrong_owner(path, *args, **kwargs):
observed = original_stat(path, *args, **kwargs)
if path == config:
fields = list(observed)
fields[4] = os.geteuid() + 1
return os.stat_result(fields)
return observed
with patch.object(Path, "lstat", new=wrong_owner):
self.assertEqual("blocked", self.tag(apply=True)["status"])
moved = self.root / "moved-access"
self.access.rename(moved)
self.access.symlink_to(moved, target_is_directory=True)
try:
self.assertEqual("blocked", self.tag(apply=True)["status"])
finally:
self.access.unlink()
moved.rename(self.access)
for flag, undo in (
("--assume-unchanged", "--no-assume-unchanged"),
("--skip-worktree", "--no-skip-worktree"),
):
with self.subTest(flag=flag):
git(self.access, "update-index", flag, "pyproject.toml")
try:
self.assertEqual("blocked", self.tag(apply=True)["status"])
self.assert_no_tags()
finally:
git(self.access, "update-index", undo, "pyproject.toml")
def test_live_remote_divergence_blocks_without_cached_tracking_update(self):
clone = self.root / "other-access-writer"
git(self.root, "clone", "--branch", "main", str(self.access_remote), str(clone))
git(clone, "config", "user.name", "Synthetic writer")
git(clone, "config", "user.email", "writer@example.invalid")
(clone / "advance.txt").write_text("new remote main\n")
git(clone, "add", ".")
git(clone, "commit", "-m", "Advance remote without updating original tracking")
git(clone, "push", "origin", "main")
result = self.tag(apply=True, push=True)
self.assertEqual("blocked", result["status"], result)
self.assertIn("live origin/main", result["repositories"][0]["detail"])
self.assert_no_tags()
def test_ignored_selected_version_metadata_cannot_supply_an_untagged_artifact(self):
project = self.access / "pyproject.toml"
original = project.read_text()
git(self.access, "rm", "pyproject.toml")
(self.access / ".gitignore").write_text("/pyproject.toml\n")
git(self.access, "add", ".gitignore")
git(self.access, "commit", "-m", "Ignored metadata absent from frozen tree")
project.write_text(original)
self.assertEqual("", git_text(self.access, "status", "--porcelain"))
result = self.tag(apply=True, push=True)
self.assertEqual("blocked", result["status"], result)
self.assertIn("must be tracked", result["repositories"][0]["detail"])
self.assert_no_tags()
def test_lightweight_and_different_annotation_objects_remain_immutable(self):
git(self.access, "tag", "v0.1.10")
self.assertEqual("blocked", self.tag(apply=True)["status"])
git(self.access, "tag", "-d", "v0.1.10")
git(self.access, "tag", "-a", "v0.1.10", "-m", "Original immutable annotation")
git(self.access, "push", "origin", "refs/tags/v0.1.10")
original = git_text(self.access_remote, "rev-parse", "refs/tags/v0.1.10")
git(self.access, "tag", "-d", "v0.1.10")
git(
self.access,
"tag",
"-a",
"v0.1.10",
"-m",
"Conflicting immutable annotation",
)
self.assertEqual("blocked", self.tag(apply=True, push=True)["status"])
self.assertEqual(
original, git_text(self.access_remote, "rev-parse", "refs/tags/v0.1.10")
)
def test_changed_source_after_preflight_stops_before_first_batch_effect(self):
original = source_tag_batch._preview_repositories
def changed(**kwargs):
result = original(**kwargs)
(self.access / "changed-source.txt").write_text(
"new source after preflight\n"
)
git(self.access, "add", ".")
git(self.access, "commit", "-m", "Source changed")
return result
with patch.object(
source_tag_batch, "_preview_repositories", side_effect=changed
):
result = self.tag(
repos=("govoplan-core", "govoplan-access"), apply=True, push=True
)
self.assertEqual("blocked", result["status"], result)
self.assertIn("receipt changed", result["repositories"][0]["detail"])
self.assert_no_tags()
def test_false_push_success_is_not_a_receipt_and_never_retries(self):
original = source_tag_batch.run
pushes = []
def run(command, **kwargs):
if command[:3] == ("git", "push", "--atomic"):
pushes.append(command)
return subprocess.CompletedProcess(command, 0, "", "")
return original(command, **kwargs)
with patch.object(source_tag_batch, "run", side_effect=run):
result = self.tag(
repos=("govoplan-access", "govoplan-core"), apply=True, push=True
)
self.assertEqual("partial", result["status"], result)
self.assertEqual(1, len(pushes))
self.assertEqual("skipped", result["repositories"][1]["status"])
self.assertFalse(ref_exists(self.core, "refs/tags/v0.1.10"))
self.assertFalse(ref_exists(self.access_remote, "refs/tags/v0.1.10"))
def _ready_bundle(self):
self.repo = self.core # Existing fixture's Core name.
campaign, remote = (
release_fixture.ReleaseRepositoryTagTests._staged_campaign_bundle(self)
)
self.specs.append(
{
"name": "govoplan-campaign",
"category": "module",
"subtype": "domain",
"path": "govoplan-campaign",
"remote": str(remote),
}
)
self.registry.write_text(json.dumps({"repositories": self.specs}))
self.assertEqual(
"tagged", self.tag(repos=("govoplan-campaign",), apply=True)["status"]
)
lock_path = self.core / "webui/package-lock.release.json"
payload = json.loads(lock_path.read_text())
package = payload["packages"]["node_modules/@govoplan/campaign-webui"]
package["version"] = self.version
package["resolved"] = (
"git+ssh://git@example.test/acme/govoplan-campaign.git#"
+ git_text(campaign, "rev-parse", "HEAD")
)
lock_path.write_text(json.dumps(payload))
# Core is a reviewed input only here: do not require an unrelated tag
# or silently impose a new clean-Core prerequisite for module release.
return campaign, remote, lock_path
def test_module_publication_freezes_core_inputs_without_requiring_core_tag(self):
_campaign, _remote, _lock = self._ready_bundle()
self.assertFalse(ref_exists(self.core, "refs/tags/v0.1.10"))
result = self.tag(repos=("govoplan-campaign",), apply=True, push=True)
self.assertEqual("published", result["status"], result)
self.assertEqual(["govoplan-campaign"], list(result["source_receipts"]))
self.assertEqual(
{"webui/package.release.json", "webui/package-lock.release.json"},
set(result["bundle_input_receipts"]),
)
self.assertFalse(ref_exists(self.core, "refs/tags/v0.1.10"))
def test_changed_or_group_writable_core_bundle_inputs_block_module_publication(
self,
):
campaign, remote, lock = self._ready_bundle()
original = source_tag_batch._preview_repositories
def changed(**kwargs):
result = original(**kwargs)
lock.write_text(lock.read_text() + "\n")
return result
with patch.object(
source_tag_batch, "_preview_repositories", side_effect=changed
):
result = self.tag(repos=("govoplan-campaign",), apply=True, push=True)
self.assertEqual("blocked", result["status"], result)
self.assertIn(
"bundle input receipt changed", result["repositories"][0]["detail"]
)
self.assertFalse(ref_exists(remote, "refs/tags/v0.1.10"))
lock.chmod(0o666)
try:
self.assertEqual(
"blocked",
self.tag(repos=("govoplan-campaign",), apply=True, push=True)["status"],
)
finally:
lock.chmod(0o644)
self.assertTrue(ref_exists(campaign, "refs/tags/v0.1.10"))
self.assertFalse(ref_exists(remote, "refs/tags/v0.1.10"))
def test_read_only_shared_preflight_has_no_apply_or_mutating_legacy_entry(self):
import inspect
from govoplan_release import repository_tag
self.assertNotIn(
"apply", inspect.signature(repository_tag._preview_repositories).parameters
)
self.assertFalse(hasattr(repository_tag, "_tag_repositories_legacy"))
if __name__ == "__main__":
unittest.main()