Harden source release preparation and record verified security follow-up
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user