diff --git a/.gitattributes b/.gitattributes index d979c26..0e40e39 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,3 +4,4 @@ docker/Dockerfile text eol=lf *.yml text eol=lf *.yaml text eol=lf ops/gitea-deploy-broker/*.py text eol=lf +scripts/*.py text eol=lf diff --git a/PUBLIC_SOURCE_EXPORT.md b/PUBLIC_SOURCE_EXPORT.md index 552db82..88472ef 100644 --- a/PUBLIC_SOURCE_EXPORT.md +++ b/PUBLIC_SOURCE_EXPORT.md @@ -1,3 +1,3 @@ # Public source export -This source snapshot was generated from the private canonical repository at revision `828f7274cafede223d37b2697367bfd89357b079`. It contains no private Git history or operational evidence. Changes are published from the canonical repository. +This source snapshot was generated from the private canonical repository at revision `402a0ef2e7d14e98325504f79ab7d7651ffc88b1`. It contains no private Git history or operational evidence. Changes are published from the canonical repository. diff --git a/PUBLIC_SOURCE_MANIFEST.json b/PUBLIC_SOURCE_MANIFEST.json index efab657..6bfe293 100644 --- a/PUBLIC_SOURCE_MANIFEST.json +++ b/PUBLIC_SOURCE_MANIFEST.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "sourceRevision": "828f7274cafede223d37b2697367bfd89357b079", + "sourceRevision": "402a0ef2e7d14e98325504f79ab7d7651ffc88b1", "files": [ { "path": ".dockerignore", @@ -14,8 +14,8 @@ }, { "path": ".gitattributes", - "sha256": "42dc3628e28e73c43dae27bc792bcb5a697de95962261d3ae2d603fd18fba206", - "bytes": 148 + "sha256": "90bd4c4cb40336c70db4c69e6737eb8de370824aac69cbc5a624da37f0e509a2", + "bytes": 173 }, { "path": ".gitignore", @@ -979,13 +979,13 @@ }, { "path": "backend/tests/test_controller_hub_release_volume.py", - "sha256": "630ba534344dd11a499f32ed6aa53816f1cefd0537a4d69f278ed2a11fa68f6a", - "bytes": 160633 + "sha256": "e13f1f01799fe74821eab9622d7eb4cf9add65196a67bde386f1c11231e04c5d", + "bytes": 161342 }, { "path": "backend/tests/test_controller_root_procedure_contract.py", - "sha256": "2e6a8645674c32f16d251b5cdcdc4f75d72759c4253aebc312b58e766b1b6581", - "bytes": 6445 + "sha256": "09fa46cc3a9510bcc8303bf9c6cf5252d3cfa676cdb1a0dad86de108d4b0e542", + "bytes": 7763 }, { "path": "backend/tests/test_controller_runtime_capabilities.py", @@ -994,8 +994,8 @@ }, { "path": "backend/tests/test_controller_state_reconciliation.py", - "sha256": "d1d90f6d4fe35e434797dccd2c0ec10357fc347900e6e148c0f85face9d12598", - "bytes": 11084 + "sha256": "b6b5e82f7818fbfa7cc2faf44dad0aaa23e812fbfa9f09f3e738c80cd74237e1", + "bytes": 11300 }, { "path": "backend/tests/test_deluge_connector.py", diff --git a/backend/tests/test_controller_hub_release_volume.py b/backend/tests/test_controller_hub_release_volume.py index 2522a26..e3fa09e 100644 --- a/backend/tests/test_controller_hub_release_volume.py +++ b/backend/tests/test_controller_hub_release_volume.py @@ -2768,7 +2768,9 @@ def test_installer_explicit_archive_and_install_contract_includes_candidate_help assert "ops/gitea-deploy-broker/prepare_mcp_hub_candidate.py" in archive_paths assert "prepare_mcp_hub_candidate.py" in installed_modules powershell = (ROOT / "scripts/upgrade_deployment_controller.ps1").read_text(encoding="utf-8") - assert "ops/gitea-deploy-broker" in powershell.split("& git archive", 1)[1].splitlines()[0] + archive_command = next(line for line in powershell.splitlines() + if line.lstrip().startswith("& git ") and " archive --format=tar " in line) + assert "ops/gitea-deploy-broker" in archive_command def test_current_release_digest_binds_the_role_password_helper(): @@ -3280,7 +3282,13 @@ def test_legacy_crlf_compatibility_is_limited_to_one_attested_handoff( assert module._matches_legacy_crlf_source(name, captured, source, identity) is accepted -def test_current_e854_crlf_capture_requires_exact_full_release_identity(): +@pytest.mark.parametrize("revision,tree,artifact", [ + ("e854334923cca574482d22b8424ba3ce46ec6aa1", "cf2a117f9222b952deb9902c646116612f3a7ea0", + "e88739bf091c8bac77ea0db5b97300666a1baa8ba984bbf6ae91e3bf25719e08"), + ("aaf7d11821a86be65c91fead97a6a4758c4fbacd", "34a90f9bd06fa4efa26a19dbf5c7c13239d03721", + "ba8de75ba04f25b5298f81d107e6108bc7a92de95531b390ade0bcba9e4678e2"), +]) +def test_current_attested_crlf_capture_requires_exact_full_release_identity(revision, tree, artifact): checked_out_source = (ROOT / "scripts/run_repopulse_adoption.py").read_bytes() source = checked_out_source.replace(b"\r\n", b"\n") captured = source.replace(b"\n", b"\r\n") @@ -3291,10 +3299,10 @@ def test_current_e854_crlf_capture_requires_exact_full_release_identity(): "e3036cef0f6e1934297de2d35c0381f93441bbf6f28bd18c94c3f47fc116e4e1" ) identity = { - "sourceRevision": "e854334923cca574482d22b8424ba3ce46ec6aa1", - "sourceTree": "cf2a117f9222b952deb9902c646116612f3a7ea0", + "sourceRevision": revision, + "sourceTree": tree, "controllerVersion": "2.1.14", - "artifactSha256": "e88739bf091c8bac77ea0db5b97300666a1baa8ba984bbf6ae91e3bf25719e08", + "artifactSha256": artifact, } assert module._matches_legacy_crlf_source( @@ -3313,17 +3321,23 @@ def test_current_e854_crlf_capture_requires_exact_full_release_identity(): ("run_repopulse_adoption.py", "", "", True), ], ) -def test_current_e854_crlf_compatibility_rejects_near_misses( - name, field, value, change_capture, +@pytest.mark.parametrize("revision,tree,artifact", [ + ("e854334923cca574482d22b8424ba3ce46ec6aa1", "cf2a117f9222b952deb9902c646116612f3a7ea0", + "e88739bf091c8bac77ea0db5b97300666a1baa8ba984bbf6ae91e3bf25719e08"), + ("aaf7d11821a86be65c91fead97a6a4758c4fbacd", "34a90f9bd06fa4efa26a19dbf5c7c13239d03721", + "ba8de75ba04f25b5298f81d107e6108bc7a92de95531b390ade0bcba9e4678e2"), +]) +def test_current_attested_crlf_compatibility_rejects_near_misses( + name, field, value, change_capture, revision, tree, artifact, ): checked_out_source = (ROOT / "scripts/run_repopulse_adoption.py").read_bytes() source = checked_out_source.replace(b"\r\n", b"\n") captured = source.replace(b"\n", b"\r\n") identity = { - "sourceRevision": "e854334923cca574482d22b8424ba3ce46ec6aa1", - "sourceTree": "cf2a117f9222b952deb9902c646116612f3a7ea0", + "sourceRevision": revision, + "sourceTree": tree, "controllerVersion": "2.1.14", - "artifactSha256": "e88739bf091c8bac77ea0db5b97300666a1baa8ba984bbf6ae91e3bf25719e08", + "artifactSha256": artifact, } if field: identity[field] = value diff --git a/backend/tests/test_controller_root_procedure_contract.py b/backend/tests/test_controller_root_procedure_contract.py index 814716d..fe9d7ac 100644 --- a/backend/tests/test_controller_root_procedure_contract.py +++ b/backend/tests/test_controller_root_procedure_contract.py @@ -1,18 +1,42 @@ from __future__ import annotations +import io import re import shlex +import subprocess +import tarfile from pathlib import Path ROOT = Path(__file__).resolve().parents[2] +def test_windows_archive_preserves_git_bytes_with_autocrlf_enabled(tmp_path): + wrapper = (ROOT / "scripts/upgrade_deployment_controller.ps1").read_text(encoding="utf-8") + command = re.search(r"& (git(?: -c \S+)*) archive --format=tar", wrapper) + assert command is not None + prefix = shlex.split(command.group(1)) + subprocess.run(["git", "init", "-q", str(tmp_path)], check=True) + source = b"print('immutable source')\n" + (tmp_path / "scripts").mkdir() + (tmp_path / "scripts/run_repopulse_adoption.py").write_bytes(source) + def git(*arguments): + return subprocess.check_output(["git", "-C", str(tmp_path), *arguments]) + git("-c", "core.autocrlf=false", "add", "scripts/run_repopulse_adoption.py") + git("-c", "commit.gpgsign=false", "-c", "user.name=Fixture", "-c", + "user.email=fixture@example.invalid", "commit", "-qm", "fixed source") + git("config", "core.autocrlf", "true") + archive = subprocess.check_output([*prefix, "-C", str(tmp_path), "archive", "--format=tar", + "HEAD", "scripts/run_repopulse_adoption.py"]) + with tarfile.open(fileobj=io.BytesIO(archive)) as packaged: + assert packaged.extractfile("scripts/run_repopulse_adoption.py").read() == source + + def test_windows_wrapper_archives_every_file_required_by_root_installer(): installer = (ROOT / "scripts/upgrade_deployment_controller.sh").read_text(encoding="utf-8") wrapper = (ROOT / "scripts/upgrade_deployment_controller.ps1").read_text(encoding="utf-8") required = re.search(r"source_paths=\(\s*(.*?)\n\)", installer, re.DOTALL) - archived = re.search(r"& git archive --format=tar .*? \$SourceRevision (.*?)\r?\n", wrapper) + archived = re.search(r"& git(?: -c \S+)* archive --format=tar .*? \$SourceRevision (.*?)\r?\n", wrapper) assert required is not None and archived is not None archive_paths = set(shlex.split(archived.group(1))) required_paths = shlex.split(required.group(1)) diff --git a/backend/tests/test_controller_state_reconciliation.py b/backend/tests/test_controller_state_reconciliation.py index 5ff396b..047540b 100644 --- a/backend/tests/test_controller_state_reconciliation.py +++ b/backend/tests/test_controller_state_reconciliation.py @@ -242,15 +242,22 @@ def test_upgrade_rolls_back_state_before_restoring_or_starting_the_old_writer(): def test_wrapper_binds_reconciliation_helper_to_the_immutable_revision(): wrapper = WRAPPER_PATH.read_text(encoding="utf-8") + archive_line = next( + line for line in wrapper.splitlines() + if re.match(r"^\s*& git(?:\s+-c\s+\S+)*\s+archive\b", line) + ) - assert "scripts/reconcile_controller_state.py" in wrapper - assert "git archive" in wrapper + assert "scripts/reconcile_controller_state.py" in archive_line + assert "$SourceRevision" in archive_line def test_wrapper_archives_the_fixed_operator_handoff_required_by_root_upgrade(): wrapper = WRAPPER_PATH.read_text(encoding="utf-8") upgrade = UPGRADE_PATH.read_text(encoding="utf-8") - archive_line = next(line for line in wrapper.splitlines() if "git archive" in line) + archive_line = next( + line for line in wrapper.splitlines() + if re.match(r"^\s*& git(?:\s+-c\s+\S+)*\s+archive\b", line) + ) source_paths_match = re.search(r"(?ms)^source_paths=\((.*?)^\)", upgrade) assert "scripts/run_repopulse_adoption.py" in archive_line