From 9c359111ea5272ee969f842e851a521d18bbcd18 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 09:44:07 +0300 Subject: [PATCH] fix(deploy): allow repeatable typed core releases --- ...d-device-control-core-release-artifact.mjs | 8 +- infra/deploy-runner/nodedc-deploy | 23 ++- ..._device_manager_control_plane_artifacts.py | 161 ++++++++++++++++++ 3 files changed, 186 insertions(+), 6 deletions(-) diff --git a/infra/deploy-runner/build-device-control-core-release-artifact.mjs b/infra/deploy-runner/build-device-control-core-release-artifact.mjs index de0615e..6124149 100644 --- a/infra/deploy-runner/build-device-control-core-release-artifact.mjs +++ b/infra/deploy-runner/build-device-control-core-release-artifact.mjs @@ -36,6 +36,10 @@ if ( } const isV2 = patchId.startsWith("device-control-core-release-v2-"); +const expectedV2Predecessor = Object.freeze({ + patchId: predecessorPatchId ?? "device-control-core-release-20260812-024", + artifactSha256: predecessorSha256 ?? "a289e909283109642e6bba3d9822a31f63423cfe0bbcd52705979681bd2bc793", +}); const descriptorPath = isV2 ? "deployment/device-control-core-release-v2.json" : "deployment/device-control-core-release-v1.json"; @@ -117,8 +121,8 @@ try { && ( descriptor.commandCatalog !== "allowlisted-adapter-typed-commands-only" || descriptor.credentialBoundary !== "transient-core-memory-then-single-pinned-mtls-command-envelope-to-edge-never-persisted-never-logged-never-returned" - || descriptor.predecessor?.patchId !== "device-control-core-release-20260812-024" - || descriptor.predecessor?.artifactSha256 !== "a289e909283109642e6bba3d9822a31f63423cfe0bbcd52705979681bd2bc793" + || descriptor.predecessor?.patchId !== expectedV2Predecessor.patchId + || descriptor.predecessor?.artifactSha256 !== expectedV2Predecessor.artifactSha256 ) ) ) { diff --git a/infra/deploy-runner/nodedc-deploy b/infra/deploy-runner/nodedc-deploy index a466019..f896693 100755 --- a/infra/deploy-runner/nodedc-deploy +++ b/infra/deploy-runner/nodedc-deploy @@ -12874,7 +12874,12 @@ def validate_device_plane_control_core_release_predecessor(payload_dir): ): die("Device Control Core release predecessor journal mismatch") - installed_release = DEVICE_PLANE_ROOT / DEVICE_PLANE_CONTROL_CORE_RELEASE_REL + installed_v1_release = ( + DEVICE_PLANE_ROOT / DEVICE_PLANE_CONTROL_CORE_RELEASE_REL + ) + installed_v2_release = ( + DEVICE_PLANE_ROOT / DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_REL + ) with tempfile.TemporaryDirectory( prefix="device-control-core-release-predecessor-", dir=TMP_DIR, @@ -12903,10 +12908,21 @@ def validate_device_plane_control_core_release_predecessor(payload_dir): DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_REL, ) ) - if installed_release.exists() or installed_release.is_symlink(): + if ( + installed_v1_release.exists() + or installed_v1_release.is_symlink() + or installed_v2_release.exists() + or installed_v2_release.is_symlink() + ): die("Device Control Core first release is already installed") else: - if tuple(entries) != DEVICE_PLANE_CONTROL_CORE_RELEASE_ENTRIES: + if tuple(entries) == DEVICE_PLANE_CONTROL_CORE_RELEASE_ENTRIES: + installed_release = installed_v1_release + source_entries = DEVICE_PLANE_CONTROL_CORE_RELEASE_ENTRIES + elif tuple(entries) == DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_ENTRIES: + installed_release = installed_v2_release + source_entries = DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_ENTRIES + else: die("Device Control Core release predecessor type mismatch") expected_installed = validate_device_plane_control_core_release_payload( predecessor_payload, @@ -12919,7 +12935,6 @@ def validate_device_plane_control_core_release_predecessor(payload_dir): ) if installed_descriptor != expected_installed: die("Device Control Core release predecessor is not current") - source_entries = DEVICE_PLANE_CONTROL_CORE_RELEASE_ENTRIES expected_source = collect_exact_files( predecessor_payload, source_entries, diff --git a/infra/deploy-runner/test_device_manager_control_plane_artifacts.py b/infra/deploy-runner/test_device_manager_control_plane_artifacts.py index 7d587f4..b93a0d7 100644 --- a/infra/deploy-runner/test_device_manager_control_plane_artifacts.py +++ b/infra/deploy-runner/test_device_manager_control_plane_artifacts.py @@ -691,6 +691,167 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase): }, ) + def test_control_core_release_v2_builder_supports_v2_release_predecessor(self): + with tempfile.TemporaryDirectory( + prefix="nodedc-control-core-v2-successor-", + ) as directory: + artifact_dir = Path(directory) / "successor" + environment = os.environ.copy() + environment["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(artifact_dir) + predecessor_id = "device-control-core-release-v2-unit-001" + predecessor_sha = "b" * 64 + completed = subprocess.run( + [ + "node", + str( + SCRIPT_DIR + / "build-device-control-core-release-artifact.mjs" + ), + "device-control-core-release-v2-unit-002", + predecessor_id, + predecessor_sha, + ], + cwd=PLATFORM_ROOT, + env=environment, + check=True, + capture_output=True, + text=True, + ) + successor = json.loads(completed.stdout) + extracted = Path(directory) / "extracted-successor" + extracted.mkdir() + _manifest, entries, payload = RUNNER.load_artifact( + Path(successor["artifact"]), + extracted, + ) + self.assertEqual( + tuple(entries), + RUNNER.DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_ENTRIES, + ) + descriptor = json.loads( + ( + payload / RUNNER.DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_REL + ).read_text(encoding="utf-8") + ) + self.assertEqual( + descriptor["predecessor"], + { + "kind": "release", + "patchId": predecessor_id, + "artifactSha256": predecessor_sha, + }, + ) + + def test_control_core_release_v2_accepts_current_v2_release_predecessor(self): + with tempfile.TemporaryDirectory( + prefix="nodedc-control-core-v2-predecessor-", + ) as directory: + root = Path(directory) + applied = root / "applied" + temporary = root / "tmp" + device_plane = root / "device-plane" + for path in (applied, temporary, device_plane / "deployment"): + path.mkdir(parents=True) + predecessor_id = "device-control-core-release-v2-unit-001" + predecessor_sha = "b" * 64 + artifact_name = f"nodedc-device-plane-{predecessor_id}.tgz" + (applied / artifact_name).write_bytes(b"reviewed-v2-predecessor") + predecessor = { + "kind": "release", + "patchId": predecessor_id, + "artifactSha256": predecessor_sha, + } + predecessor_descriptor = ( + RUNNER.expected_device_plane_control_core_release_descriptor( + predecessor_id, + { + "kind": "release", + "patchId": "device-control-core-release-20260812-024", + "artifactSha256": "a" * 64, + }, + schema_version="v2", + ) + ) + candidate_descriptor = ( + RUNNER.expected_device_plane_control_core_release_descriptor( + "device-control-core-release-v2-unit-002", + predecessor, + schema_version="v2", + ) + ) + installed = ( + device_plane / RUNNER.DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_REL + ) + installed.write_text( + json.dumps(predecessor_descriptor), + encoding="utf-8", + ) + with ( + mock.patch.object(RUNNER, "APPLIED_DIR", applied), + mock.patch.object(RUNNER, "TMP_DIR", temporary), + mock.patch.object(RUNNER, "DEVICE_PLANE_ROOT", device_plane), + mock.patch.object( + RUNNER, + "validate_device_plane_control_core_release_payload", + side_effect=(candidate_descriptor, predecessor_descriptor), + ), + mock.patch.object( + RUNNER, + "load_artifact", + return_value=( + { + "id": predecessor_id, + "component": "device-plane", + "type": "app-overlay", + }, + RUNNER.DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_ENTRIES, + root / "predecessor-payload", + ), + ), + mock.patch.object( + RUNNER, + "load_state", + return_value=[{ + "id": predecessor_id, + "artifact": artifact_name, + "component": "device-plane", + "sha256": predecessor_sha, + "status": "ok", + }], + ), + mock.patch.object( + RUNNER, + "sha256_file", + return_value=predecessor_sha, + ), + mock.patch.object( + RUNNER, + "collect_exact_files", + return_value={"same": "source"}, + ), + mock.patch.object(RUNNER, "component_compose_files"), + mock.patch.object( + RUNNER, + "inspect_device_edge_channel_core_identity_state", + return_value="valid-reuse-at-apply", + ), + mock.patch.object(RUNNER, "healthcheck_compose_service"), + mock.patch.object( + RUNNER, + "validate_device_manager_control_plane_runtime", + ), + ): + result = ( + RUNNER.validate_device_plane_control_core_release_predecessor( + root / "candidate-payload" + ) + ) + + self.assertEqual( + result["mode"], + "active-device-control-core-forward-release", + ) + def test_control_core_release_rejects_direct_legacy_predecessor(self): with tempfile.TemporaryDirectory( prefix="nodedc-control-core-release-invalid-",